feat(t226): add Go ERP session login
This commit is contained in:
@@ -269,7 +269,8 @@ func safeNext(value string) string {
|
||||
if parsed.Path != "/tasks" &&
|
||||
!strings.HasPrefix(parsed.Path, "/tasks/") &&
|
||||
parsed.Path != "/freight" &&
|
||||
!strings.HasPrefix(parsed.Path, "/freight/") {
|
||||
!strings.HasPrefix(parsed.Path, "/freight/") &&
|
||||
parsed.Path != "/erp" {
|
||||
return "/tasks"
|
||||
}
|
||||
return parsed.String()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1095,6 +1095,105 @@ func TestFreightSyncToNowIgnoresPrefilledManualDates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestERPConnectionPageUsesCaptchaOnlyAndPreservesNoCredentials(t *testing.T) {
|
||||
ticket := mustToken(t)
|
||||
service := &fakeERPService{
|
||||
fakeService: &fakeService{},
|
||||
status: ERPConnectionStatus{
|
||||
Configured: true,
|
||||
},
|
||||
image: ERPCaptchaImage{
|
||||
Content: []byte("captcha-image"),
|
||||
ContentType: "image/png",
|
||||
},
|
||||
ticket: ticket,
|
||||
}
|
||||
router := newTestRouter(t, service)
|
||||
page := performRequest(t, router, http.MethodGet, "/erp", nil, "")
|
||||
if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), "获取验证码") ||
|
||||
strings.Contains(page.Body.String(), "private-password") {
|
||||
t.Fatalf("ERP page = %d / %s", page.Code, page.Body)
|
||||
}
|
||||
assertSecurityHeaders(t, page)
|
||||
cookie := csrfCookie(t, page)
|
||||
values := url.Values{"csrf_token": {cookie.Value}}
|
||||
request := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/erp/captcha",
|
||||
strings.NewReader(values.Encode()),
|
||||
)
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
router.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusSeeOther ||
|
||||
response.Header().Get("Location") != "/erp?notice=captcha-ready" ||
|
||||
service.captchaRequests != 1 {
|
||||
t.Fatalf("captcha response/calls = %d / %q / %d", response.Code, response.Header().Get("Location"), service.captchaRequests)
|
||||
}
|
||||
service.status.CaptchaReady = true
|
||||
service.status.CaptchaTicket = ticket
|
||||
page = performRequest(t, router, http.MethodGet, "/erp", nil, "")
|
||||
if page.Code != http.StatusOK ||
|
||||
!strings.Contains(page.Body.String(), "/erp/captcha/"+ticket) ||
|
||||
!strings.Contains(page.Body.String(), `name="captcha_code"`) ||
|
||||
strings.Contains(page.Body.String(), "password") {
|
||||
t.Fatalf("captcha page = %d / %s", page.Code, page.Body)
|
||||
}
|
||||
image := performRequest(t, router, http.MethodGet, "/erp/captcha/"+ticket, nil, "")
|
||||
if image.Code != http.StatusOK || image.Header().Get("Cache-Control") != "no-store" ||
|
||||
image.Body.String() != "captcha-image" {
|
||||
t.Fatalf("captcha image = %d / %q / %s", image.Code, image.Header(), image.Body)
|
||||
}
|
||||
cookie = csrfCookie(t, page)
|
||||
values = url.Values{
|
||||
"csrf_token": {cookie.Value},
|
||||
"captcha_ticket": {ticket},
|
||||
"captcha_code": {"1234"},
|
||||
}
|
||||
request = httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/erp/login",
|
||||
strings.NewReader(values.Encode()),
|
||||
)
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.AddCookie(cookie)
|
||||
response = httptest.NewRecorder()
|
||||
router.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusSeeOther ||
|
||||
response.Header().Get("Location") != "/erp?notice=login-succeeded" ||
|
||||
service.login.CaptchaTicket != ticket || service.login.CaptchaCode != "1234" {
|
||||
t.Fatalf("login response/input = %d / %q / %+v", response.Code, response.Header().Get("Location"), service.login)
|
||||
}
|
||||
service.status = ERPConnectionStatus{
|
||||
Configured: true,
|
||||
CaptchaReady: true,
|
||||
CaptchaTicket: ticket,
|
||||
}
|
||||
service.err = ErrERPLoginRejected
|
||||
page = performRequest(t, router, http.MethodGet, "/erp", nil, "")
|
||||
cookie = csrfCookie(t, page)
|
||||
values = url.Values{
|
||||
"csrf_token": {cookie.Value},
|
||||
"captcha_ticket": {ticket},
|
||||
"captcha_code": {"1234"},
|
||||
}
|
||||
request = httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/erp/login",
|
||||
strings.NewReader(values.Encode()),
|
||||
)
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.AddCookie(cookie)
|
||||
response = httptest.NewRecorder()
|
||||
router.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusUnprocessableEntity ||
|
||||
!strings.Contains(response.Body.String(), "验证码不正确或 ERP 拒绝登录") ||
|
||||
strings.Contains(response.Body.String(), "private") {
|
||||
t.Fatalf("rejected login page = %d / %s", response.Code, response.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
||||
const itemID = "00000000-0000-4000-8000-000000000002"
|
||||
service := &fakeProcurementService{
|
||||
@@ -1206,6 +1305,51 @@ type fakeFreightService struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type fakeERPService struct {
|
||||
*fakeService
|
||||
status ERPConnectionStatus
|
||||
image ERPCaptchaImage
|
||||
ticket string
|
||||
captchaRequests int
|
||||
login ERPLoginInput
|
||||
err error
|
||||
}
|
||||
|
||||
func (service *fakeERPService) ERPConnectionStatus(
|
||||
context.Context,
|
||||
) (ERPConnectionStatus, error) {
|
||||
return service.status, service.err
|
||||
}
|
||||
|
||||
func (service *fakeERPService) RequestERPCaptcha(
|
||||
context.Context,
|
||||
) (ERPConnectionStatus, error) {
|
||||
service.captchaRequests++
|
||||
service.status.CaptchaReady = true
|
||||
service.status.CaptchaTicket = service.ticket
|
||||
return service.status, service.err
|
||||
}
|
||||
|
||||
func (service *fakeERPService) OpenERPCaptcha(
|
||||
context.Context,
|
||||
string,
|
||||
) (ERPCaptchaImage, error) {
|
||||
return service.image, service.err
|
||||
}
|
||||
|
||||
func (service *fakeERPService) LoginERP(
|
||||
_ context.Context,
|
||||
input ERPLoginInput,
|
||||
) (ERPConnectionStatus, error) {
|
||||
service.login = input
|
||||
if service.err == nil {
|
||||
service.status.Authenticated = true
|
||||
service.status.CaptchaReady = false
|
||||
service.status.CaptchaTicket = ""
|
||||
}
|
||||
return service.status, service.err
|
||||
}
|
||||
|
||||
type fakeProcurementService struct {
|
||||
*fakeFreightService
|
||||
createRequestInput CreateProcurementRequestInput
|
||||
|
||||
@@ -258,6 +258,20 @@ select {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.erp-captcha-image {
|
||||
display: block;
|
||||
width: 180px;
|
||||
height: 64px;
|
||||
margin: 12px 0;
|
||||
border: 1px solid var(--line-strong);
|
||||
background: var(--surface);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.compact-form {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
a,
|
||||
button,
|
||||
input,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{{define "erp-connection"}}
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>{{.Page.Title}} - 采购任务管理</title>
|
||||
{{template "document-head" .}}
|
||||
</head>
|
||||
<body>
|
||||
{{template "site-header" .}}
|
||||
<main id="main-content" class="page narrow-page">
|
||||
<div class="title-row">
|
||||
<div>
|
||||
<h1>ERP 连接</h1>
|
||||
<p class="subtitle">建立本次服务进程内的顺运宝会话</p>
|
||||
</div>
|
||||
<a class="button" href="/freight/import">返回导入</a>
|
||||
</div>
|
||||
{{if .Error}}<div class="notice danger" role="alert">{{.Error}}</div>{{end}}
|
||||
{{if .Notice}}<div class="notice success" role="status">{{.Notice}}</div>{{end}}
|
||||
{{if .Status.Authenticated}}
|
||||
<section class="detail-section" aria-labelledby="erp-connected-title">
|
||||
<h2 id="erp-connected-title">已连接</h2>
|
||||
<p>当前后端进程持有受控 ERP 会话。重启后需要重新获取验证码登录。</p>
|
||||
</section>
|
||||
{{else if not .Status.Configured}}
|
||||
<section class="detail-section" aria-labelledby="erp-config-title">
|
||||
<h2 id="erp-config-title">尚未配置</h2>
|
||||
<p>请在后端启动环境中配置顺运宝账号和密码后重启服务。</p>
|
||||
</section>
|
||||
{{else if .Status.CaptchaReady}}
|
||||
<section class="detail-section" aria-labelledby="erp-captcha-title">
|
||||
<h2 id="erp-captcha-title">输入验证码</h2>
|
||||
<img class="erp-captcha-image" src="/erp/captcha/{{pathPart .Status.CaptchaTicket}}"
|
||||
alt="ERP 验证码" width="180" height="64">
|
||||
<form class="form-panel compact-form" method="post" action="/erp/login" data-loading-form>
|
||||
<input type="hidden" name="csrf_token" value="{{.Page.CSRFToken}}">
|
||||
<input type="hidden" name="captcha_ticket" value="{{.Status.CaptchaTicket}}">
|
||||
<div class="field">
|
||||
<label for="captcha-code">验证码</label>
|
||||
<input id="captcha-code" name="captcha_code" maxlength="64" autocomplete="one-time-code" required>
|
||||
</div>
|
||||
<button class="button primary" type="submit" data-loading-label="正在登录…">登录 ERP</button>
|
||||
</form>
|
||||
</section>
|
||||
{{else}}
|
||||
<form class="form-panel" method="post" action="/erp/captcha" data-loading-form>
|
||||
<input type="hidden" name="csrf_token" value="{{.Page.CSRFToken}}">
|
||||
<h2>获取验证码</h2>
|
||||
<p class="secondary">验证码一次有效,仅用于当前后端进程中的 ERP 会话。</p>
|
||||
<button class="button primary" type="submit" data-loading-label="正在获取…">获取验证码</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="/tasks" {{if .Page.TasksCurrent}}aria-current="page"{{end}}>任务列表</a>
|
||||
<a href="/tasks/new" {{if .Page.NewCurrent}}aria-current="page"{{end}}>新建任务</a>
|
||||
<a href="/freight" {{if .Page.FreightCurrent}}aria-current="page"{{end}}>ERP 货运</a>
|
||||
<a href="/erp" {{if .Page.ERPCurrent}}aria-current="page"{{end}}>ERP 连接</a>
|
||||
</nav>
|
||||
{{if .Page.CSRFToken}}
|
||||
<form class="logout-form" method="post" action="/logout">
|
||||
|
||||
@@ -8,12 +8,17 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrForbidden = errors.New("resource forbidden")
|
||||
ErrConflict = errors.New("resource conflict")
|
||||
ErrValidation = errors.New("validation failed")
|
||||
ErrInvalidFile = errors.New("invalid file")
|
||||
ErrUnavailable = errors.New("service unavailable")
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrForbidden = errors.New("resource forbidden")
|
||||
ErrConflict = errors.New("resource conflict")
|
||||
ErrValidation = errors.New("validation failed")
|
||||
ErrInvalidFile = errors.New("invalid file")
|
||||
ErrUnavailable = errors.New("service unavailable")
|
||||
ErrERPNotConfigured = errors.New("ERP is not configured")
|
||||
ErrERPSessionNeeded = errors.New("ERP session is required")
|
||||
ErrERPCaptchaInvalid = errors.New("ERP captcha is invalid")
|
||||
ErrERPLoginRejected = errors.New("ERP login was rejected")
|
||||
ErrERPProtocol = errors.New("ERP protocol is invalid")
|
||||
)
|
||||
|
||||
// Service is the application boundary required by the server-rendered admin UI.
|
||||
@@ -38,6 +43,30 @@ type FreightService interface {
|
||||
) (FreightSync, error)
|
||||
}
|
||||
|
||||
type ERPConnectionService interface {
|
||||
ERPConnectionStatus(context.Context) (ERPConnectionStatus, error)
|
||||
RequestERPCaptcha(context.Context) (ERPConnectionStatus, error)
|
||||
OpenERPCaptcha(context.Context, string) (ERPCaptchaImage, error)
|
||||
LoginERP(context.Context, ERPLoginInput) (ERPConnectionStatus, error)
|
||||
}
|
||||
|
||||
type ERPConnectionStatus struct {
|
||||
Configured bool
|
||||
Authenticated bool
|
||||
CaptchaReady bool
|
||||
CaptchaTicket string
|
||||
}
|
||||
|
||||
type ERPCaptchaImage struct {
|
||||
Content []byte
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type ERPLoginInput struct {
|
||||
CaptchaTicket string
|
||||
CaptchaCode string
|
||||
}
|
||||
|
||||
type ProcurementService interface {
|
||||
CreateProcurementRequest(
|
||||
context.Context,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"cmroubao/backend-api/internal/domain"
|
||||
"cmroubao/backend-api/internal/platform/shunyunbao"
|
||||
"cmroubao/backend-api/internal/transport/authcommon"
|
||||
"cmroubao/backend-api/internal/usecase"
|
||||
)
|
||||
@@ -19,6 +20,7 @@ type UsecaseAdapter struct {
|
||||
authorizations *usecase.OrderAuthorizationService
|
||||
freight *usecase.FreightService
|
||||
procurement *usecase.ProcurementService
|
||||
erp *shunyunbao.SessionManager
|
||||
}
|
||||
|
||||
func (adapter *UsecaseAdapter) SetProcurement(
|
||||
@@ -27,6 +29,65 @@ func (adapter *UsecaseAdapter) SetProcurement(
|
||||
adapter.procurement = procurement
|
||||
}
|
||||
|
||||
func (adapter *UsecaseAdapter) SetERPConnection(
|
||||
manager *shunyunbao.SessionManager,
|
||||
) {
|
||||
adapter.erp = manager
|
||||
}
|
||||
|
||||
func (adapter *UsecaseAdapter) ERPConnectionStatus(
|
||||
context.Context,
|
||||
) (ERPConnectionStatus, error) {
|
||||
if adapter.erp == nil {
|
||||
return ERPConnectionStatus{}, ErrUnavailable
|
||||
}
|
||||
return erpConnectionStatusFrom(adapter.erp.Status()), nil
|
||||
}
|
||||
|
||||
func (adapter *UsecaseAdapter) RequestERPCaptcha(
|
||||
ctx context.Context,
|
||||
) (ERPConnectionStatus, error) {
|
||||
if adapter.erp == nil {
|
||||
return ERPConnectionStatus{}, ErrUnavailable
|
||||
}
|
||||
status, err := adapter.erp.FetchCaptcha(ctx)
|
||||
if err != nil {
|
||||
return erpConnectionStatusFrom(status), mapERPError(err)
|
||||
}
|
||||
return erpConnectionStatusFrom(status), nil
|
||||
}
|
||||
|
||||
func (adapter *UsecaseAdapter) OpenERPCaptcha(
|
||||
_ context.Context,
|
||||
ticket string,
|
||||
) (ERPCaptchaImage, error) {
|
||||
if adapter.erp == nil {
|
||||
return ERPCaptchaImage{}, ErrUnavailable
|
||||
}
|
||||
image, err := adapter.erp.OpenCaptcha(ticket)
|
||||
if err != nil {
|
||||
return ERPCaptchaImage{}, mapERPError(err)
|
||||
}
|
||||
return ERPCaptchaImage{
|
||||
Content: image.Content,
|
||||
ContentType: image.ContentType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (adapter *UsecaseAdapter) LoginERP(
|
||||
ctx context.Context,
|
||||
input ERPLoginInput,
|
||||
) (ERPConnectionStatus, error) {
|
||||
if adapter.erp == nil {
|
||||
return ERPConnectionStatus{}, ErrUnavailable
|
||||
}
|
||||
status, err := adapter.erp.Login(ctx, input.CaptchaTicket, input.CaptchaCode)
|
||||
if err != nil {
|
||||
return erpConnectionStatusFrom(status), mapERPError(err)
|
||||
}
|
||||
return erpConnectionStatusFrom(status), nil
|
||||
}
|
||||
|
||||
func NewUsecaseAdapter(
|
||||
tasks *usecase.TaskService,
|
||||
assets *usecase.AssetService,
|
||||
@@ -840,6 +901,38 @@ func mapUsecaseError(err error) error {
|
||||
}
|
||||
}
|
||||
|
||||
func mapERPError(err error) error {
|
||||
var public error
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrFreightSourceNotConfigured):
|
||||
public = ErrERPNotConfigured
|
||||
case errors.Is(err, domain.ErrFreightSourceSessionNeeded):
|
||||
public = ErrERPSessionNeeded
|
||||
case errors.Is(err, shunyunbao.ErrCaptchaTicketInvalid):
|
||||
public = ErrERPCaptchaInvalid
|
||||
case errors.Is(err, shunyunbao.ErrLoginRejected):
|
||||
public = ErrERPLoginRejected
|
||||
case errors.Is(err, domain.ErrFreightSourceProtocol):
|
||||
public = ErrERPProtocol
|
||||
case errors.Is(err, domain.ErrFreightSourceUnavailable):
|
||||
public = ErrUnavailable
|
||||
default:
|
||||
return err
|
||||
}
|
||||
return &adapterError{public: public, cause: err}
|
||||
}
|
||||
|
||||
func erpConnectionStatusFrom(
|
||||
status shunyunbao.SessionStatus,
|
||||
) ERPConnectionStatus {
|
||||
return ERPConnectionStatus{
|
||||
Configured: status.Configured,
|
||||
Authenticated: status.Authenticated,
|
||||
CaptchaReady: status.CaptchaReady,
|
||||
CaptchaTicket: status.CaptchaTicket,
|
||||
}
|
||||
}
|
||||
|
||||
type adapterError struct {
|
||||
public error
|
||||
cause error
|
||||
@@ -856,3 +949,4 @@ func (err *adapterError) Unwrap() []error {
|
||||
var _ Service = (*UsecaseAdapter)(nil)
|
||||
var _ FreightService = (*UsecaseAdapter)(nil)
|
||||
var _ ProcurementService = (*UsecaseAdapter)(nil)
|
||||
var _ ERPConnectionService = (*UsecaseAdapter)(nil)
|
||||
|
||||
Reference in New Issue
Block a user