package httpapi import ( "errors" "net/http" "strconv" "strings" "unicode/utf8" "cmroubao/backend-api/internal/domain" "cmroubao/backend-api/internal/platform/shunyunbao" "github.com/gin-gonic/gin" ) func registerERPAdminAPI(routes gin.IRoutes, handler *adminHandlers) { routes.GET("/api/v1/erp-session", handler.erpSessionStatus) routes.POST("/api/v1/erp-session/captcha", handler.createERPCaptcha) routes.GET( "/api/v1/erp-session/captcha/:ticket", handler.erpCaptchaContent, ) routes.POST("/api/v1/erp-session/login", handler.loginERP) } func (h *adminHandlers) erpSessionStatus(ctx *gin.Context) { ctx.Header("Cache-Control", "no-store") ctx.JSON(http.StatusOK, erpSessionResponse(h.services.ERP.Status())) } func (h *adminHandlers) createERPCaptcha(ctx *gin.Context) { status, err := h.services.ERP.FetchCaptcha(ctx.Request.Context()) if err != nil { writeERPError(ctx, err) return } ctx.Header("Cache-Control", "no-store") ctx.JSON(http.StatusOK, gin.H{ "session": erpSessionResponse(status), "captcha_ticket": status.CaptchaTicket, "captcha_url": "/api/v1/erp-session/captcha/" + status.CaptchaTicket, }) } func (h *adminHandlers) erpCaptchaContent(ctx *gin.Context) { ticket := strings.TrimSpace(ctx.Param("ticket")) if !validERPTicket(ticket) { writeERPError(ctx, shunyunbao.ErrCaptchaTicketInvalid) return } image, err := h.services.ERP.OpenCaptcha(ticket) if err != nil || !strings.HasPrefix(image.ContentType, "image/") || len(image.Content) == 0 { writeERPError(ctx, err) return } ctx.Header("Cache-Control", "no-store") ctx.Header("Content-Type", image.ContentType) ctx.Header("Content-Length", strconv.Itoa(len(image.Content))) ctx.Header("Content-Disposition", "inline") ctx.Data(http.StatusOK, image.ContentType, image.Content) } func (h *adminHandlers) loginERP(ctx *gin.Context) { if !hasMediaType(ctx, "application/json") { writePublicError( ctx, http.StatusUnsupportedMediaType, "UNSUPPORTED_MEDIA_TYPE", "application/json is required", false, gin.H{}, ) return } var request struct { CaptchaTicket string `json:"captcha_ticket"` CaptchaCode string `json:"captcha_code"` } if err := decodeJSON(ctx, &request); err != nil || !validERPTicket(request.CaptchaTicket) || !validERPCaptchaCode(request.CaptchaCode) { writePublicError( ctx, http.StatusBadRequest, "ERP_LOGIN_INVALID", "ERP login request is invalid", false, gin.H{}, ) return } status, err := h.services.ERP.Login( ctx.Request.Context(), request.CaptchaTicket, request.CaptchaCode, ) if err != nil { writeERPError(ctx, err) return } ctx.Header("Cache-Control", "no-store") ctx.JSON(http.StatusOK, erpSessionResponse(status)) } func erpSessionResponse(status shunyunbao.SessionStatus) gin.H { return gin.H{ "configured": status.Configured, "authenticated": status.Authenticated, "captcha_ready": status.CaptchaReady, } } func validERPTicket(value string) bool { value = strings.TrimSpace(value) return len(value) == 43 && utf8.ValidString(value) && !strings.ContainsAny(value, " \t\r\n") } func validERPCaptchaCode(value string) bool { value = strings.TrimSpace(value) if value == "" || len([]byte(value)) > 64 || !utf8.ValidString(value) { return false } for _, character := range value { if character < 32 || character == 127 { return false } } return true } func writeERPError(ctx *gin.Context, err error) { status := http.StatusInternalServerError code := "ERP_INTERNAL_ERROR" message := "ERP connection operation failed" retryable := false switch { case errors.Is(err, domain.ErrFreightSourceNotConfigured): status = http.StatusUnprocessableEntity code = "ERP_NOT_CONFIGURED" message = "ERP credentials are not configured" case errors.Is(err, domain.ErrFreightSourceSessionNeeded): status = http.StatusConflict code = "ERP_SESSION_REQUIRED" message = "ERP session is required" case errors.Is(err, shunyunbao.ErrCaptchaTicketInvalid): status = http.StatusConflict code = "ERP_CAPTCHA_INVALID" message = "ERP captcha must be requested again" case errors.Is(err, shunyunbao.ErrLoginRejected): status = http.StatusUnprocessableEntity code = "ERP_LOGIN_REJECTED" message = "ERP login was rejected" case errors.Is(err, domain.ErrFreightSourceProtocol): status = http.StatusBadGateway code = "ERP_RESPONSE_INVALID" message = "ERP response is invalid" case errors.Is(err, domain.ErrFreightSourceUnavailable): status = http.StatusServiceUnavailable code = "ERP_UNAVAILABLE" message = "ERP is temporarily unavailable" retryable = true } writePublicError(ctx, status, code, message, retryable, gin.H{}) }