1582 lines
39 KiB
Go
1582 lines
39 KiB
Go
package handler
|
||
|
||
import (
|
||
"database/sql"
|
||
"errors"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/shopspring/decimal"
|
||
"github.com/wmms/wmms/internal/middleware"
|
||
"github.com/wmms/wmms/internal/model"
|
||
"github.com/wmms/wmms/internal/repository"
|
||
"github.com/wmms/wmms/internal/service"
|
||
"github.com/wmms/wmms/pkg/response"
|
||
)
|
||
|
||
type Handler struct {
|
||
materialSvc *service.MaterialService
|
||
inboundSvc *service.InboundService
|
||
outboundSvc *service.OutboundService
|
||
stockSvc *service.StockService
|
||
logSvc *service.MaterialLogService
|
||
authSvc *service.AuthService
|
||
appSvc *service.ApplicationService
|
||
organizationSvc *service.OrganizationService
|
||
masterDataSvc *service.MasterDataService
|
||
partnerSvc *service.PartnerService
|
||
warehouseSvc *service.WarehouseService
|
||
}
|
||
|
||
func userFacingServiceErrorMessage(err error) string {
|
||
if errors.Is(err, service.ErrInsufficientStock) {
|
||
return "库存不足"
|
||
}
|
||
return err.Error()
|
||
}
|
||
|
||
func NewHandler() *Handler {
|
||
return &Handler{
|
||
materialSvc: service.NewMaterialService(),
|
||
inboundSvc: service.NewInboundService(),
|
||
outboundSvc: service.NewOutboundService(),
|
||
stockSvc: service.NewStockService(),
|
||
logSvc: service.NewMaterialLogService(),
|
||
authSvc: service.NewAuthService(),
|
||
appSvc: service.NewApplicationService(),
|
||
organizationSvc: service.NewOrganizationService(),
|
||
masterDataSvc: service.NewMasterDataService(),
|
||
partnerSvc: service.NewPartnerService(),
|
||
warehouseSvc: service.NewWarehouseService(),
|
||
}
|
||
}
|
||
|
||
func (h *Handler) RegisterRoutes(r *gin.Engine) {
|
||
// Health check (public)
|
||
r.GET("/api/health", func(c *gin.Context) {
|
||
c.JSON(200, gin.H{"status": "ok"})
|
||
})
|
||
|
||
api := r.Group("/api")
|
||
{
|
||
// Protected upload route
|
||
upload := api.Group("/upload")
|
||
upload.Use(middleware.AuthMiddleware())
|
||
{
|
||
upload.POST("", h.UploadImage)
|
||
}
|
||
|
||
// Auth routes (public, with rate limiting)
|
||
auth := api.Group("/auth")
|
||
auth.Use(middleware.RateLimitMiddleware())
|
||
{
|
||
auth.POST("/login", h.Login)
|
||
}
|
||
|
||
// Protected routes
|
||
logs := api.Group("/logs")
|
||
logs.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
logs.GET("", h.ListLogs)
|
||
logs.GET("/materials/:materialId", h.GetLogsByMaterial)
|
||
}
|
||
|
||
materials := api.Group("/materials")
|
||
materials.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
materials.GET("", h.ListMaterials)
|
||
materials.GET("/:id", h.GetMaterial)
|
||
materials.POST("", h.CreateMaterial)
|
||
materials.PUT("/:id", h.UpdateMaterial)
|
||
materials.POST("/:id/obsolete", h.ObsoleteMaterial)
|
||
materials.POST("/:id/restore", h.RestoreMaterial)
|
||
}
|
||
|
||
inbound := api.Group("/inbound")
|
||
inbound.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
inbound.GET("", h.ListInbound)
|
||
inbound.GET("/:id", h.GetInbound)
|
||
inbound.POST("", h.CreateInbound)
|
||
inbound.PUT("/:id", h.UpdateInbound)
|
||
inbound.DELETE("/:id", h.DeleteInbound)
|
||
inbound.GET("/materials/:materialId", h.GetInboundByMaterial)
|
||
}
|
||
|
||
outbound := api.Group("/outbound")
|
||
outbound.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
outbound.GET("", h.ListOutbound)
|
||
outbound.GET("/:id", h.GetOutbound)
|
||
outbound.POST("", h.CreateOutbound)
|
||
outbound.PUT("/:id", h.UpdateOutbound)
|
||
outbound.DELETE("/:id", h.DeleteOutbound)
|
||
outbound.GET("/materials/:materialId", h.GetOutboundByMaterial)
|
||
}
|
||
|
||
stock := api.Group("/stock")
|
||
stock.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
stock.GET("", h.ListStock)
|
||
stock.GET("/warning", h.GetWarningStock)
|
||
stock.GET("/logs", h.GetAllStockLogs)
|
||
stock.GET("/logs/:materialId", h.GetStockLogs)
|
||
stock.PUT("/:materialId/warning", h.UpdateWarningStock)
|
||
stock.GET("/:materialId/details", h.GetInventoryBalanceDetails)
|
||
stock.GET("/:materialId", h.GetStock)
|
||
}
|
||
|
||
// Admin only routes
|
||
users := api.Group("/users")
|
||
users.Use(middleware.AuthMiddleware(), middleware.AdminOnly())
|
||
{
|
||
users.GET("", h.ListUsers)
|
||
users.POST("", h.CreateUser)
|
||
users.PUT("/:id", h.UpdateUser)
|
||
users.DELETE("/:id", h.DeleteUser)
|
||
}
|
||
|
||
organizations := api.Group("/organizations")
|
||
organizations.Use(middleware.AuthMiddleware(), middleware.AdminOnly())
|
||
{
|
||
organizations.GET("", h.ListOrganizations)
|
||
organizations.POST("", h.CreateOrganization)
|
||
organizations.PUT("/:id", h.UpdateOrganization)
|
||
}
|
||
|
||
roles := api.Group("/roles")
|
||
roles.Use(middleware.AuthMiddleware(), middleware.AdminOnly())
|
||
{
|
||
roles.GET("", h.ListRoles)
|
||
roles.PUT("/:id", h.UpdateRole)
|
||
}
|
||
|
||
permissions := api.Group("/permissions")
|
||
permissions.Use(middleware.AuthMiddleware(), middleware.AdminOnly())
|
||
{
|
||
permissions.GET("", h.ListPermissions)
|
||
}
|
||
|
||
categories := api.Group("/material-categories")
|
||
categories.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
categories.GET("", h.ListMaterialCategories)
|
||
categories.POST("", middleware.AdminOnly(), h.CreateMaterialCategory)
|
||
categories.PUT("/:id", middleware.AdminOnly(), h.UpdateMaterialCategory)
|
||
}
|
||
|
||
units := api.Group("/units")
|
||
units.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
units.GET("", h.ListUnits)
|
||
units.POST("", middleware.AdminOnly(), h.CreateUnit)
|
||
units.PUT("/:id", middleware.AdminOnly(), h.UpdateUnit)
|
||
}
|
||
|
||
codeRules := api.Group("/code-rules")
|
||
codeRules.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
codeRules.GET("", h.ListCodeRules)
|
||
codeRules.POST("", middleware.AdminOnly(), h.CreateCodeRule)
|
||
codeRules.PUT("/:id", middleware.AdminOnly(), h.UpdateCodeRule)
|
||
}
|
||
|
||
// 编码预览单独放开给普通用户:新增物料时要靠它得到自动编码,
|
||
// 而编码规则本身的增删改仍然只有管理员能做。
|
||
codeRulePreview := api.Group("/code-rules")
|
||
codeRulePreview.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
codeRulePreview.GET("/next", h.GetNextMaterialCode)
|
||
}
|
||
|
||
partners := api.Group("/partners")
|
||
partners.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware(), middleware.AdminOnly())
|
||
{
|
||
partners.GET("", h.ListPartners)
|
||
partners.POST("", h.CreatePartner)
|
||
partners.PUT("/:id", h.UpdatePartner)
|
||
}
|
||
|
||
warehouses := api.Group("/warehouses")
|
||
warehouses.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware(), middleware.AdminOnly())
|
||
{
|
||
warehouses.GET("", h.ListWarehouses)
|
||
warehouses.POST("", h.CreateWarehouse)
|
||
warehouses.PUT("/:id", h.UpdateWarehouse)
|
||
warehouses.POST("/:id/locations", h.CreateWarehouseLocation)
|
||
warehouses.PUT("/:id/locations/:locationId", h.UpdateWarehouseLocation)
|
||
}
|
||
|
||
// Application routes (user: create/cancel own, list own; admin: list all, approve/reject)
|
||
applications := api.Group("/applications")
|
||
applications.Use(middleware.AuthMiddleware(), middleware.OrganizationMiddleware())
|
||
{
|
||
applications.POST("", h.CreateApplication)
|
||
applications.GET("/my", h.ListMyApplications)
|
||
applications.POST("/:id/cancel", h.CancelApplication)
|
||
applications.GET("", h.ListApplications) // admin only
|
||
applications.POST("/:id/approve", h.ApproveApplication) // admin only
|
||
applications.POST("/:id/reject", h.RejectApplication) // admin only
|
||
}
|
||
|
||
// Current user route
|
||
me := api.Group("/auth")
|
||
me.Use(middleware.AuthMiddleware())
|
||
{
|
||
me.GET("/organizations", h.ListAccessibleOrganizations)
|
||
me.POST("/organization-context", h.SwitchOrganizationContext)
|
||
me.GET("/me", h.GetCurrentUser)
|
||
me.POST("/logout", h.Logout)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Material handlers
|
||
|
||
func (h *Handler) ListMaterials(c *gin.Context) {
|
||
search := c.Query("search")
|
||
status := c.DefaultQuery("status", "active")
|
||
if status != "active" && status != "obsolete" && status != "all" {
|
||
response.BadRequest(c, "status must be active, obsolete, or all")
|
||
return
|
||
}
|
||
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
materials, total, err := h.materialSvc.List(orgID, search, status, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if materials == nil {
|
||
materials = []model.Material{}
|
||
}
|
||
response.PaginatedSuccess(c, materials, limit, offset, total)
|
||
}
|
||
|
||
func (h *Handler) GetMaterial(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
material, err := h.materialSvc.GetByID(orgID, id)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Material not found")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.Success(c, material)
|
||
}
|
||
|
||
func (h *Handler) CreateMaterial(c *gin.Context) {
|
||
var req model.CreateMaterialRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
req.OrganizationID = orgID
|
||
|
||
operator := middleware.GetCurrentUsername(c)
|
||
if operator == "" {
|
||
operator = "系统"
|
||
}
|
||
|
||
material, err := h.materialSvc.Create(&req, operator)
|
||
if err != nil {
|
||
if errors.Is(err, service.ErrInvalidMaterialCategory) || errors.Is(err, service.ErrInvalidMaterialUnit) {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if errors.Is(err, repository.ErrDuplicateMaterialCode) {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.Created(c, "Material created successfully", material)
|
||
}
|
||
|
||
func (h *Handler) UpdateMaterial(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
|
||
var req model.UpdateMaterialRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
operator := middleware.GetCurrentUsername(c)
|
||
if operator == "" {
|
||
operator = "系统"
|
||
}
|
||
|
||
material, err := h.materialSvc.Update(orgID, id, &req, operator)
|
||
if err != nil {
|
||
if errors.Is(err, service.ErrInvalidMaterialCategory) || errors.Is(err, service.ErrInvalidMaterialUnit) {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Material not found")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "Material updated successfully", material)
|
||
}
|
||
|
||
func (h *Handler) DeleteMaterial(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
operator := middleware.GetCurrentUsername(c)
|
||
if operator == "" {
|
||
operator = "系统"
|
||
}
|
||
|
||
err = h.materialSvc.Delete(orgID, id, operator)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Material not found")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "Material deleted successfully", nil)
|
||
}
|
||
|
||
func (h *Handler) ObsoleteMaterial(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
operator := middleware.GetCurrentUsername(c)
|
||
if operator == "" {
|
||
operator = "系统"
|
||
}
|
||
if err := h.materialSvc.Obsolete(orgID, id, operator); err != nil {
|
||
if strings.Contains(err.Error(), "仍有库存") {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
// 不存在或不属于本公司:读取阶段返回 sql.ErrNoRows,事务阶段返回"物料不存在",
|
||
// 两者都按 404 处理,不能让跨公司操作看起来像服务器错误。
|
||
if errors.Is(err, sql.ErrNoRows) || strings.Contains(err.Error(), "物料不存在") {
|
||
response.Error(c, 404, "物料不存在")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "Material obsoleted successfully", nil)
|
||
}
|
||
|
||
func (h *Handler) RestoreMaterial(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
operator := middleware.GetCurrentUsername(c)
|
||
if operator == "" {
|
||
operator = "系统"
|
||
}
|
||
if err := h.materialSvc.Restore(orgID, id, operator); err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) || strings.Contains(err.Error(), "物料不存在") {
|
||
response.Error(c, 404, "物料不存在")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "Material restored successfully", nil)
|
||
}
|
||
|
||
// Inbound handlers
|
||
|
||
func (h *Handler) ListInbound(c *gin.Context) {
|
||
search := c.Query("search")
|
||
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
orders, total, err := h.inboundSvc.List(orgID, search, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if orders == nil {
|
||
orders = []model.InboundOrder{}
|
||
}
|
||
response.PaginatedSuccess(c, orders, limit, offset, total)
|
||
}
|
||
|
||
func (h *Handler) GetInbound(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid inbound order id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
order, err := h.inboundSvc.GetByID(orgID, id)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Inbound order not found")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.Success(c, order)
|
||
}
|
||
|
||
func (h *Handler) CreateInbound(c *gin.Context) {
|
||
var req model.CreateInboundRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
operator := middleware.GetCurrentUsername(c)
|
||
if operator == "" {
|
||
operator = "系统"
|
||
}
|
||
order, err := h.inboundSvc.Create(orgID, &req, operator)
|
||
if err != nil {
|
||
if errors.Is(err, service.ErrMaterialNotFound) {
|
||
response.BadRequest(c, "material not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrMaterialObsolete) {
|
||
response.BadRequest(c, "material is obsolete")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInventoryScopeUnavailable) {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
// Broadcast stock update to all connected clients(带上公司归属,供按公司投递过滤)
|
||
GetHub().BroadcastStockUpdate("inbound", orgID, order.ID, order)
|
||
response.Created(c, "Inbound order created successfully", order)
|
||
}
|
||
|
||
func (h *Handler) DeleteInbound(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid inbound order id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
err = h.inboundSvc.Delete(orgID, id)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Inbound order not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInventoryScopeUnavailable) {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "Inbound order deleted successfully", nil)
|
||
}
|
||
|
||
func (h *Handler) UpdateInbound(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid inbound order id")
|
||
return
|
||
}
|
||
|
||
var req model.UpdateInboundRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
order, err := h.inboundSvc.Update(orgID, id, &req)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Inbound order not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrMaterialNotFound) {
|
||
response.BadRequest(c, "material not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrMaterialObsolete) {
|
||
response.BadRequest(c, "material is obsolete")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInventoryScopeUnavailable) {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
GetHub().BroadcastStockUpdate("inbound", orgID, order.ID, order)
|
||
response.Success(c, order)
|
||
}
|
||
|
||
func (h *Handler) GetInboundByMaterial(c *gin.Context) {
|
||
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
records, err := h.inboundSvc.GetByMaterialID(orgID, materialID)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if records == nil {
|
||
records = []model.InboundRecord{}
|
||
}
|
||
response.Success(c, records)
|
||
}
|
||
|
||
// Outbound handlers
|
||
|
||
func (h *Handler) ListOutbound(c *gin.Context) {
|
||
search := c.Query("search")
|
||
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
orders, total, err := h.outboundSvc.List(orgID, search, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if orders == nil {
|
||
orders = []model.OutboundOrder{}
|
||
}
|
||
response.PaginatedSuccess(c, orders, limit, offset, total)
|
||
}
|
||
|
||
func (h *Handler) GetOutbound(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid outbound order id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
order, err := h.outboundSvc.GetByID(orgID, id)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Outbound order not found")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.Success(c, order)
|
||
}
|
||
|
||
func (h *Handler) CreateOutbound(c *gin.Context) {
|
||
var req model.CreateOutboundRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
operator := middleware.GetCurrentUsername(c)
|
||
if operator == "" {
|
||
operator = "系统"
|
||
}
|
||
order, err := h.outboundSvc.Create(orgID, &req, operator)
|
||
if err != nil {
|
||
if errors.Is(err, service.ErrMaterialNotFound) {
|
||
response.BadRequest(c, "material not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrMaterialObsolete) {
|
||
response.BadRequest(c, "material is obsolete")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInsufficientStock) {
|
||
response.BadRequest(c, userFacingServiceErrorMessage(err))
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInventoryScopeUnavailable) {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
// Broadcast stock update to all connected clients(带上公司归属,供按公司投递过滤)
|
||
GetHub().BroadcastStockUpdate("outbound", orgID, order.ID, order)
|
||
response.Created(c, "Outbound order created successfully", order)
|
||
}
|
||
|
||
func (h *Handler) DeleteOutbound(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid outbound order id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
err = h.outboundSvc.Delete(orgID, id)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Outbound order not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInventoryScopeUnavailable) {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "Outbound order deleted successfully", nil)
|
||
}
|
||
|
||
func (h *Handler) UpdateOutbound(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid outbound order id")
|
||
return
|
||
}
|
||
|
||
var req model.UpdateOutboundRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
operator := middleware.GetCurrentUsername(c)
|
||
if operator == "" {
|
||
operator = "系统"
|
||
}
|
||
order, err := h.outboundSvc.Update(orgID, id, &req, operator)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Outbound order not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrMaterialNotFound) {
|
||
response.BadRequest(c, "material not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrMaterialObsolete) {
|
||
response.BadRequest(c, "material is obsolete")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInsufficientStock) {
|
||
response.BadRequest(c, userFacingServiceErrorMessage(err))
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInventoryScopeUnavailable) {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
GetHub().BroadcastStockUpdate("outbound", orgID, order.ID, order)
|
||
response.Success(c, order)
|
||
}
|
||
|
||
func (h *Handler) GetOutboundByMaterial(c *gin.Context) {
|
||
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
records, err := h.outboundSvc.GetByMaterialID(orgID, materialID)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if records == nil {
|
||
records = []model.OutboundRecord{}
|
||
}
|
||
response.Success(c, records)
|
||
}
|
||
|
||
// Stock handlers
|
||
|
||
func (h *Handler) ListStock(c *gin.Context) {
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
search := c.Query("search")
|
||
// 大类编码筛选,前端下拉选中的就是 material_categories 里的大类 code
|
||
category := c.Query("category")
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
stocks, total, err := h.stockSvc.GetAllStock(orgID, search, category, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if stocks == nil {
|
||
stocks = []model.Stock{}
|
||
}
|
||
response.PaginatedSuccess(c, stocks, limit, offset, total)
|
||
}
|
||
|
||
func (h *Handler) GetStock(c *gin.Context) {
|
||
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
stock, err := h.stockSvc.GetByMaterialID(orgID, materialID)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
response.NotFound(c, "Material not found")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.Success(c, stock)
|
||
}
|
||
|
||
// GetInventoryBalanceDetails 返回当前公司某物料的库存维度明细
|
||
func (h *Handler) GetInventoryBalanceDetails(c *gin.Context) {
|
||
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
exists, err := h.stockSvc.MaterialExists(orgID, materialID)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if !exists {
|
||
response.NotFound(c, "Material not found")
|
||
return
|
||
}
|
||
|
||
details, err := h.stockSvc.GetInventoryBalanceDetails(orgID, materialID)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if details == nil {
|
||
details = []model.InventoryBalanceDetail{}
|
||
}
|
||
response.Success(c, details)
|
||
}
|
||
|
||
func (h *Handler) GetWarningStock(c *gin.Context) {
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
stocks, total, err := h.stockSvc.GetWarningStock(orgID, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if stocks == nil {
|
||
stocks = []model.Stock{}
|
||
}
|
||
response.PaginatedSuccess(c, stocks, limit, offset, total)
|
||
}
|
||
|
||
func (h *Handler) UpdateWarningStock(c *gin.Context) {
|
||
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
var req struct {
|
||
WarningStock decimal.Decimal `json:"warning_stock"`
|
||
// Enabled 用指针接收:不传表示沿用旧行为(启用),只有显式传 false 才是关闭
|
||
// 该物料的预警。
|
||
Enabled *bool `json:"enabled"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if req.WarningStock.IsNegative() {
|
||
response.BadRequest(c, "warning_stock must be greater than or equal to 0")
|
||
return
|
||
}
|
||
enabled := true
|
||
if req.Enabled != nil {
|
||
enabled = *req.Enabled
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
// 先校验物料归属:预警规则的外键指向全局 materials(id),不校验的话写别家公司的
|
||
// material_id 会静默成功。跨公司一律 404,与库存详情、流水保持一致。
|
||
exists, err := h.stockSvc.MaterialExists(orgID, materialID)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if !exists {
|
||
response.NotFound(c, "Material not found")
|
||
return
|
||
}
|
||
|
||
// 改变库存域的写操作必须落在当前公司启用的仓库、库位上;缺失或停用时回 409 并给出
|
||
// 具体配置提示,而不是写进默认公司或静默成功。
|
||
scope, err := service.ResolveInventoryScope(orgID, true)
|
||
if err != nil {
|
||
if errors.Is(err, service.ErrInventoryScopeUnavailable) {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if err := h.stockSvc.UpdateWarningStock(scope, materialID, req.WarningStock, enabled); err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "Stock warning updated successfully", nil)
|
||
}
|
||
|
||
func (h *Handler) GetStockLogs(c *gin.Context) {
|
||
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
exists, err := h.stockSvc.MaterialExists(orgID, materialID)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if !exists {
|
||
response.NotFound(c, "Material not found")
|
||
return
|
||
}
|
||
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
logs, total, err := h.stockSvc.GetLogsByMaterialID(orgID, materialID, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if logs == nil {
|
||
logs = []model.StockLogResponse{}
|
||
}
|
||
response.PaginatedSuccess(c, logs, limit, offset, total)
|
||
}
|
||
|
||
func (h *Handler) GetAllStockLogs(c *gin.Context) {
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
logs, total, err := h.stockSvc.GetAllLogs(orgID, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if logs == nil {
|
||
logs = []model.StockLogResponse{}
|
||
}
|
||
response.PaginatedSuccess(c, logs, limit, offset, total)
|
||
}
|
||
|
||
// Material Log handlers
|
||
|
||
func (h *Handler) ListLogs(c *gin.Context) {
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
operationType := c.Query("operation_type")
|
||
materialId := c.Query("material_id")
|
||
startDate := c.Query("start_date")
|
||
endDate := c.Query("end_date")
|
||
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
logs, total, err := h.logSvc.List(orgID, operationType, materialId, startDate, endDate, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if logs == nil {
|
||
logs = []model.MaterialLog{}
|
||
}
|
||
response.PaginatedSuccess(c, logs, limit, offset, total)
|
||
}
|
||
|
||
func (h *Handler) GetLogsByMaterial(c *gin.Context) {
|
||
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid material id")
|
||
return
|
||
}
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
logs, err := h.logSvc.GetByMaterialID(orgID, materialID)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if logs == nil {
|
||
logs = []model.MaterialLog{}
|
||
}
|
||
response.Success(c, logs)
|
||
}
|
||
|
||
// Auth handlers
|
||
|
||
func (h *Handler) Login(c *gin.Context) {
|
||
ip := c.ClientIP()
|
||
|
||
// Check if IP is rate limited before processing
|
||
if middleware.IsRateLimited(ip) {
|
||
response.Error(c, 429, "too many login attempts, please try again later")
|
||
return
|
||
}
|
||
|
||
var req model.LoginRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
result, err := h.authSvc.Login(req.Username, req.Password)
|
||
if err != nil {
|
||
// Record failed attempt
|
||
middleware.RecordFailedAttempt(ip)
|
||
response.Unauthorized(c, "invalid username or password")
|
||
return
|
||
}
|
||
|
||
// Reset attempts on successful login
|
||
middleware.ResetAttempts(ip)
|
||
response.Success(c, result)
|
||
}
|
||
|
||
func (h *Handler) Logout(c *gin.Context) {
|
||
// For simple token auth, logout is handled client-side by removing the token
|
||
response.SuccessWithMessage(c, "logout successful", nil)
|
||
}
|
||
|
||
func (h *Handler) GetCurrentUser(c *gin.Context) {
|
||
userID := middleware.GetCurrentUserID(c)
|
||
|
||
user, err := h.authSvc.GetUserByID(userID)
|
||
if err != nil {
|
||
response.NotFound(c, "user not found")
|
||
return
|
||
}
|
||
|
||
response.Success(c, user)
|
||
}
|
||
|
||
// User management handlers (Admin only)
|
||
|
||
func (h *Handler) ListUsers(c *gin.Context) {
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
search := c.Query("search")
|
||
|
||
users, total, err := h.authSvc.ListUsers(search, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
if users == nil {
|
||
users = []model.User{}
|
||
}
|
||
response.PaginatedSuccess(c, users, limit, offset, total)
|
||
}
|
||
|
||
func (h *Handler) CreateUser(c *gin.Context) {
|
||
var req struct {
|
||
model.UserOrganizationInput
|
||
Username string `json:"username" binding:"required"`
|
||
Password string `json:"password" binding:"required"`
|
||
RoleIDs []int64 `json:"role_ids" binding:"required,min=1"`
|
||
Description string `json:"description"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
req.ActorUserID = middleware.GetCurrentUserID(c)
|
||
user, err := h.authSvc.CreateUser(req.Username, req.Password, req.Description, req.RoleIDs, req.UserOrganizationInput)
|
||
if err != nil {
|
||
if message := roleErrorText(err); message != "" {
|
||
response.BadRequest(c, message)
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
|
||
response.Created(c, "user created successfully", user)
|
||
}
|
||
|
||
func (h *Handler) UpdateUser(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid user id")
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
model.UserOrganizationInput
|
||
Password string `json:"password"`
|
||
RoleIDs []int64 `json:"role_ids" binding:"required,min=1"`
|
||
Description string `json:"description"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
req.ActorUserID = middleware.GetCurrentUserID(c)
|
||
err = h.authSvc.UpdateUser(id, req.Description, req.RoleIDs, req.Password, req.UserOrganizationInput)
|
||
if err != nil {
|
||
if message := roleErrorText(err); message != "" {
|
||
response.BadRequest(c, message)
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "user updated successfully", nil)
|
||
}
|
||
|
||
func (h *Handler) DeleteUser(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid user id")
|
||
return
|
||
}
|
||
|
||
// Prevent deleting yourself
|
||
currentUserID := middleware.GetCurrentUserID(c)
|
||
if currentUserID == id {
|
||
response.BadRequest(c, "cannot delete yourself")
|
||
return
|
||
}
|
||
|
||
if err := h.authSvc.DeleteUser(id); err != nil {
|
||
if message := roleErrorText(err); message != "" {
|
||
response.BadRequest(c, message)
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "user deleted successfully", nil)
|
||
}
|
||
|
||
// roleErrorText 把角色相关的仓储层错误翻成面向用户的中文,返回空字符串表示这个错误
|
||
// 与角色无关。翻译放在 handler 而非仓储层:仓储层的错误常量是给开发者看的标识,不该
|
||
// 承担界面文案。前端的 utils/errorText.js 只放行带中文的消息,英文会被换成笼统的
|
||
// "请求参数有误",用户看不出问题出在角色上。
|
||
func roleErrorText(err error) string {
|
||
switch {
|
||
case errors.Is(err, repository.ErrInvalidOrganizations):
|
||
return err.Error()
|
||
case errors.Is(err, repository.ErrInvalidRoles):
|
||
return "所选角色无效或已被停用,请重新选择"
|
||
case errors.Is(err, repository.ErrLastSuperAdmin):
|
||
return "不能移除最后一个超级管理员"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// Application handlers
|
||
|
||
func (h *Handler) CreateApplication(c *gin.Context) {
|
||
userID := middleware.GetCurrentUserID(c)
|
||
|
||
var req model.CreateApplicationRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
app, err := h.appSvc.Create(orgID, userID, &req)
|
||
if err != nil {
|
||
if errors.Is(err, service.ErrMaterialNotFound) || errors.Is(err, service.ErrMaterialObsolete) {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
|
||
response.Created(c, "application created successfully", app)
|
||
}
|
||
|
||
func (h *Handler) ListMyApplications(c *gin.Context) {
|
||
userID := middleware.GetCurrentUserID(c)
|
||
status := c.Query("status")
|
||
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
apps, total, err := h.appSvc.ListByUser(orgID, userID, status, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.PaginatedSuccess(c, apps, limit, offset, int64(total))
|
||
}
|
||
|
||
func (h *Handler) ListApplications(c *gin.Context) {
|
||
if !middleware.IsSuperAdmin(c) {
|
||
response.Unauthorized(c, "admin only")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
status := c.Query("status")
|
||
|
||
limit := 20
|
||
offset := 0
|
||
if l := c.Query("limit"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||
limit = parsed
|
||
if limit > 100 {
|
||
limit = 100
|
||
}
|
||
}
|
||
}
|
||
if o := c.Query("offset"); o != "" {
|
||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
apps, total, err := h.appSvc.ListAll(orgID, status, limit, offset)
|
||
if err != nil {
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
response.PaginatedSuccess(c, apps, limit, offset, int64(total))
|
||
}
|
||
|
||
func (h *Handler) CancelApplication(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid application id")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
userID := middleware.GetCurrentUserID(c)
|
||
if err := h.appSvc.Cancel(orgID, id, userID); err != nil {
|
||
if errors.Is(err, service.ErrApplicationNotFound) {
|
||
response.NotFound(c, "application not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrCannotCancelOthers) || errors.Is(err, service.ErrCannotCancelApplication) {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "application cancelled successfully", nil)
|
||
}
|
||
|
||
func (h *Handler) ApproveApplication(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid application id")
|
||
return
|
||
}
|
||
|
||
if !middleware.IsSuperAdmin(c) {
|
||
response.Unauthorized(c, "admin only")
|
||
return
|
||
}
|
||
|
||
var req model.ApproveApplicationRequest
|
||
c.ShouldBindJSON(&req) // optional
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
approverID := middleware.GetCurrentUserID(c)
|
||
if err := h.appSvc.Approve(orgID, id, approverID, req.AutoOutbound); err != nil {
|
||
if err == service.ErrApplicationNotFound {
|
||
response.NotFound(c, "application not found")
|
||
return
|
||
}
|
||
if err == service.ErrApplicationNotPending {
|
||
response.BadRequest(c, "application is not pending")
|
||
return
|
||
}
|
||
if err == service.ErrInsufficientStock {
|
||
response.BadRequest(c, userFacingServiceErrorMessage(err))
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrInventoryScopeUnavailable) {
|
||
response.Error(c, 409, err.Error())
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "application approved successfully", nil)
|
||
}
|
||
|
||
func (h *Handler) RejectApplication(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "invalid application id")
|
||
return
|
||
}
|
||
|
||
if !middleware.IsSuperAdmin(c) {
|
||
response.Unauthorized(c, "admin only")
|
||
return
|
||
}
|
||
|
||
orgID, ok := organizationID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
var req model.RejectApplicationRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "rejection reason is required")
|
||
return
|
||
}
|
||
|
||
if err := h.appSvc.Reject(orgID, id, req.Reason); err != nil {
|
||
if errors.Is(err, service.ErrApplicationNotFound) {
|
||
response.NotFound(c, "application not found")
|
||
return
|
||
}
|
||
if errors.Is(err, service.ErrApplicationNotPending) {
|
||
response.BadRequest(c, "application is not pending")
|
||
return
|
||
}
|
||
response.InternalError(c, err.Error())
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "application rejected successfully", nil)
|
||
}
|