feat(t226): add Go ERP session login
This commit is contained in:
@@ -77,6 +77,12 @@ func (h *Handler) RegisterProtected(routes gin.IRoutes) {
|
||||
routes.POST("/freight/import", SecurityHeaders(), h.CreateFreightImport)
|
||||
routes.GET("/freight/:id", SecurityHeaders(), h.FreightDetail)
|
||||
}
|
||||
if _, ok := h.service.(ERPConnectionService); ok {
|
||||
routes.GET("/erp", SecurityHeaders(), h.ERPConnection)
|
||||
routes.POST("/erp/captcha", SecurityHeaders(), h.RequestERPCaptcha)
|
||||
routes.GET("/erp/captcha/:ticket", SecurityHeaders(), h.ERPCaptchaImage)
|
||||
routes.POST("/erp/login", SecurityHeaders(), h.LoginERP)
|
||||
}
|
||||
if _, ok := h.service.(ProcurementService); ok {
|
||||
routes.POST(
|
||||
"/freight/items/:id/procurement-request",
|
||||
@@ -96,6 +102,195 @@ func (h *Handler) RegisterProtected(routes gin.IRoutes) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ERPConnection(ctx *gin.Context) {
|
||||
service := h.service.(ERPConnectionService)
|
||||
status, err := service.ERPConnectionStatus(ctx.Request.Context())
|
||||
if err != nil {
|
||||
h.renderERPConnection(
|
||||
ctx,
|
||||
http.StatusServiceUnavailable,
|
||||
ERPConnectionStatus{},
|
||||
"ERP 连接状态暂时无法读取。",
|
||||
"",
|
||||
)
|
||||
return
|
||||
}
|
||||
h.renderERPConnection(
|
||||
ctx,
|
||||
http.StatusOK,
|
||||
status,
|
||||
"",
|
||||
erpConnectionNotice(ctx.Query("notice")),
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Handler) RequestERPCaptcha(ctx *gin.Context) {
|
||||
ctx.Request.Body = http.MaxBytesReader(
|
||||
ctx.Writer,
|
||||
ctx.Request.Body,
|
||||
maxLoginFormBytes,
|
||||
)
|
||||
if err := ctx.Request.ParseForm(); err != nil || !validCSRF(ctx) {
|
||||
h.renderError(ctx, http.StatusForbidden, "请求已失效", "请刷新 ERP 连接页面后重试。")
|
||||
return
|
||||
}
|
||||
service := h.service.(ERPConnectionService)
|
||||
status, err := service.RequestERPCaptcha(ctx.Request.Context())
|
||||
if err != nil {
|
||||
h.renderERPConnection(
|
||||
ctx,
|
||||
erpConnectionErrorStatus(err),
|
||||
status,
|
||||
erpConnectionErrorMessage(err),
|
||||
"",
|
||||
)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(http.StatusSeeOther, "/erp?notice=captcha-ready")
|
||||
}
|
||||
|
||||
func (h *Handler) ERPCaptchaImage(ctx *gin.Context) {
|
||||
ticket := strings.TrimSpace(ctx.Param("ticket"))
|
||||
if !validToken(ticket) {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
service := h.service.(ERPConnectionService)
|
||||
image, err := service.OpenERPCaptcha(ctx.Request.Context(), ticket)
|
||||
if err != nil || !strings.HasPrefix(image.ContentType, "image/") ||
|
||||
len(image.Content) == 0 {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
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 *Handler) LoginERP(ctx *gin.Context) {
|
||||
ctx.Request.Body = http.MaxBytesReader(
|
||||
ctx.Writer,
|
||||
ctx.Request.Body,
|
||||
maxLoginFormBytes,
|
||||
)
|
||||
if err := ctx.Request.ParseForm(); err != nil || !validCSRF(ctx) {
|
||||
h.renderError(ctx, http.StatusForbidden, "请求已失效", "请刷新 ERP 连接页面后重试。")
|
||||
return
|
||||
}
|
||||
ticket := strings.TrimSpace(ctx.PostForm("captcha_ticket"))
|
||||
code := strings.TrimSpace(ctx.PostForm("captcha_code"))
|
||||
service := h.service.(ERPConnectionService)
|
||||
if !validToken(ticket) || !validERPCaptchaCode(code) {
|
||||
status, _ := service.ERPConnectionStatus(ctx.Request.Context())
|
||||
h.renderERPConnection(
|
||||
ctx,
|
||||
http.StatusUnprocessableEntity,
|
||||
status,
|
||||
"请重新获取验证码后输入验证码。",
|
||||
"",
|
||||
)
|
||||
return
|
||||
}
|
||||
status, err := service.LoginERP(ctx.Request.Context(), ERPLoginInput{
|
||||
CaptchaTicket: ticket,
|
||||
CaptchaCode: code,
|
||||
})
|
||||
if err != nil {
|
||||
h.renderERPConnection(
|
||||
ctx,
|
||||
erpConnectionErrorStatus(err),
|
||||
status,
|
||||
erpConnectionErrorMessage(err),
|
||||
"",
|
||||
)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(http.StatusSeeOther, "/erp?notice=login-succeeded")
|
||||
}
|
||||
|
||||
func (h *Handler) renderERPConnection(
|
||||
ctx *gin.Context,
|
||||
statusCode int,
|
||||
status ERPConnectionStatus,
|
||||
errorMessage, notice string,
|
||||
) {
|
||||
token, err := csrfToken(ctx)
|
||||
if err != nil {
|
||||
h.renderError(ctx, http.StatusInternalServerError, "页面暂时无法打开", "请稍后重试。")
|
||||
return
|
||||
}
|
||||
h.render(ctx, statusCode, "erp-connection", erpConnectionPage{
|
||||
Page: pageView{
|
||||
Title: "ERP 连接",
|
||||
ERPCurrent: true,
|
||||
CSRFToken: token,
|
||||
},
|
||||
Status: status,
|
||||
Error: errorMessage,
|
||||
Notice: notice,
|
||||
})
|
||||
}
|
||||
|
||||
func validERPCaptchaCode(value string) bool {
|
||||
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 erpConnectionErrorStatus(err error) int {
|
||||
switch {
|
||||
case errors.Is(err, ErrERPNotConfigured):
|
||||
return http.StatusUnprocessableEntity
|
||||
case errors.Is(err, ErrERPSessionNeeded),
|
||||
errors.Is(err, ErrERPCaptchaInvalid):
|
||||
return http.StatusConflict
|
||||
case errors.Is(err, ErrERPLoginRejected):
|
||||
return http.StatusUnprocessableEntity
|
||||
case errors.Is(err, ErrERPProtocol):
|
||||
return http.StatusBadGateway
|
||||
case errors.Is(err, ErrUnavailable):
|
||||
return http.StatusServiceUnavailable
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
func erpConnectionErrorMessage(err error) string {
|
||||
switch {
|
||||
case errors.Is(err, ErrERPNotConfigured):
|
||||
return "ERP 服务账号尚未在后端启动环境中配置。"
|
||||
case errors.Is(err, ErrERPSessionNeeded), errors.Is(err, ErrERPCaptchaInvalid):
|
||||
return "验证码已失效,请重新获取后再登录。"
|
||||
case errors.Is(err, ErrERPLoginRejected):
|
||||
return "验证码不正确或 ERP 拒绝登录,请重新获取验证码后重试。"
|
||||
case errors.Is(err, ErrERPProtocol):
|
||||
return "ERP 返回格式无法确认,请稍后重试。"
|
||||
case errors.Is(err, ErrUnavailable):
|
||||
return "ERP 暂时不可用,请稍后重试。"
|
||||
default:
|
||||
return "ERP 连接操作失败,请稍后重试。"
|
||||
}
|
||||
}
|
||||
|
||||
func erpConnectionNotice(value string) string {
|
||||
switch value {
|
||||
case "captcha-ready":
|
||||
return "验证码已获取,请人工读取并提交。"
|
||||
case "login-succeeded":
|
||||
return "ERP 会话已建立,可以返回货运导入。"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ListFreight(ctx *gin.Context) {
|
||||
service := h.service.(FreightService)
|
||||
orders, err := service.ListFreightOrders(
|
||||
@@ -1087,6 +1282,7 @@ type pageView struct {
|
||||
TasksCurrent bool
|
||||
NewCurrent bool
|
||||
FreightCurrent bool
|
||||
ERPCurrent bool
|
||||
CSRFToken string
|
||||
}
|
||||
|
||||
@@ -1113,6 +1309,13 @@ type freightDetailPage struct {
|
||||
Notice string
|
||||
}
|
||||
|
||||
type erpConnectionPage struct {
|
||||
Page pageView
|
||||
Status ERPConnectionStatus
|
||||
Error string
|
||||
Notice string
|
||||
}
|
||||
|
||||
type statusOption struct {
|
||||
Value string
|
||||
Label string
|
||||
|
||||
Reference in New Issue
Block a user