feat(t223): generate tasks from freight items
This commit is contained in:
@@ -77,6 +77,23 @@ 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.(ProcurementService); ok {
|
||||
routes.POST(
|
||||
"/freight/items/:id/procurement-request",
|
||||
SecurityHeaders(),
|
||||
h.CreateFreightProcurementRequest,
|
||||
)
|
||||
routes.POST(
|
||||
"/freight/procurement-requests/:id/reference",
|
||||
SecurityHeaders(),
|
||||
h.BindFreightProcurementReference,
|
||||
)
|
||||
routes.POST(
|
||||
"/freight/procurement-requests/:id/purchase-task",
|
||||
SecurityHeaders(),
|
||||
h.CreateFreightProcurementTask,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ListFreight(ctx *gin.Context) {
|
||||
@@ -193,6 +210,23 @@ func (h *Handler) FreightDetail(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
token, _ := csrfToken(ctx)
|
||||
for index := range detail.Items {
|
||||
if detail.Items[index].Request == nil {
|
||||
continue
|
||||
}
|
||||
uploadKey, keyErr := newToken()
|
||||
if keyErr != nil {
|
||||
h.renderError(ctx, http.StatusInternalServerError, "页面暂时无法打开", "请稍后重试。")
|
||||
return
|
||||
}
|
||||
taskKey, keyErr := newToken()
|
||||
if keyErr != nil {
|
||||
h.renderError(ctx, http.StatusInternalServerError, "页面暂时无法打开", "请稍后重试。")
|
||||
return
|
||||
}
|
||||
detail.Items[index].Request.UploadKey = uploadKey
|
||||
detail.Items[index].Request.TaskKey = taskKey
|
||||
}
|
||||
h.render(ctx, http.StatusOK, "freight-detail", freightDetailPage{
|
||||
Page: pageView{
|
||||
Title: "货运详情",
|
||||
@@ -200,9 +234,148 @@ func (h *Handler) FreightDetail(ctx *gin.Context) {
|
||||
CSRFToken: token,
|
||||
},
|
||||
Detail: detail,
|
||||
Notice: freightNotice(ctx.Query("notice")),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateFreightProcurementRequest(ctx *gin.Context) {
|
||||
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 16<<10)
|
||||
if err := ctx.Request.ParseForm(); err != nil || !validCSRF(ctx) {
|
||||
h.renderError(ctx, http.StatusForbidden, "请求已失效", "请返回货运详情后重新操作。")
|
||||
return
|
||||
}
|
||||
orderID := strings.TrimSpace(ctx.PostForm("order_id"))
|
||||
if pathEscape(orderID) == "invalid" ||
|
||||
ctx.PostForm("confirm_procurement_needed") != "1" {
|
||||
h.renderError(ctx, http.StatusUnprocessableEntity, "必须人工确认", "请核对来源商品后确认仍需采购。")
|
||||
return
|
||||
}
|
||||
service := h.service.(ProcurementService)
|
||||
_, err := service.CreateProcurementRequest(
|
||||
ctx.Request.Context(),
|
||||
CreateProcurementRequestInput{
|
||||
ActorUserID: actorUserID(ctx.Request.Context()),
|
||||
FreightOrderItemID: strings.TrimSpace(ctx.Param("id")),
|
||||
ConfirmProcurementNeeded: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrConflict) || errors.Is(err, ErrValidation) {
|
||||
ctx.Redirect(
|
||||
http.StatusSeeOther,
|
||||
"/freight/"+pathEscape(orderID)+"?notice=request-conflict",
|
||||
)
|
||||
return
|
||||
}
|
||||
h.renderServiceError(ctx, err, "采购需求创建失败,请稍后重试。")
|
||||
return
|
||||
}
|
||||
ctx.Redirect(
|
||||
http.StatusSeeOther,
|
||||
"/freight/"+pathEscape(orderID)+"?notice=request-created",
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Handler) BindFreightProcurementReference(ctx *gin.Context) {
|
||||
ctx.Request.Body = http.MaxBytesReader(
|
||||
ctx.Writer,
|
||||
ctx.Request.Body,
|
||||
maxRequestBytes,
|
||||
)
|
||||
if err := ctx.Request.ParseMultipartForm(maxRequestBytes); err != nil ||
|
||||
!validCSRF(ctx) {
|
||||
h.renderError(ctx, http.StatusForbidden, "请求已失效", "请返回货运详情后重新操作。")
|
||||
return
|
||||
}
|
||||
if ctx.Request.MultipartForm != nil {
|
||||
defer ctx.Request.MultipartForm.RemoveAll()
|
||||
}
|
||||
orderID := strings.TrimSpace(ctx.PostForm("order_id"))
|
||||
uploadKey := strings.TrimSpace(ctx.PostForm("upload_key"))
|
||||
if pathEscape(orderID) == "invalid" || !validToken(uploadKey) {
|
||||
h.renderError(ctx, http.StatusForbidden, "请求已失效", "请返回货运详情后重新操作。")
|
||||
return
|
||||
}
|
||||
asset, err := h.uploadReference(ctx, uploadKey)
|
||||
if err != nil {
|
||||
ctx.Redirect(
|
||||
http.StatusSeeOther,
|
||||
"/freight/"+pathEscape(orderID)+"?notice=image-invalid",
|
||||
)
|
||||
return
|
||||
}
|
||||
service := h.service.(ProcurementService)
|
||||
_, err = service.BindProcurementReference(
|
||||
ctx.Request.Context(),
|
||||
BindProcurementReferenceInput{
|
||||
ActorUserID: actorUserID(ctx.Request.Context()),
|
||||
RequestID: strings.TrimSpace(ctx.Param("id")),
|
||||
ImageAssetID: asset.ID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
ctx.Redirect(
|
||||
http.StatusSeeOther,
|
||||
"/freight/"+pathEscape(orderID)+"?notice=reference-conflict",
|
||||
)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(
|
||||
http.StatusSeeOther,
|
||||
"/freight/"+pathEscape(orderID)+"?notice=reference-bound",
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateFreightProcurementTask(ctx *gin.Context) {
|
||||
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 16<<10)
|
||||
if err := ctx.Request.ParseForm(); err != nil || !validCSRF(ctx) {
|
||||
h.renderError(ctx, http.StatusForbidden, "请求已失效", "请返回货运详情后重新操作。")
|
||||
return
|
||||
}
|
||||
orderID := strings.TrimSpace(ctx.PostForm("order_id"))
|
||||
taskKey := strings.TrimSpace(ctx.PostForm("task_key"))
|
||||
if pathEscape(orderID) == "invalid" || !validToken(taskKey) {
|
||||
h.renderError(ctx, http.StatusForbidden, "请求已失效", "请返回货运详情后重新操作。")
|
||||
return
|
||||
}
|
||||
service := h.service.(ProcurementService)
|
||||
task, err := service.CreateProcurementTask(
|
||||
ctx.Request.Context(),
|
||||
CreateProcurementTaskInput{
|
||||
ActorUserID: actorUserID(ctx.Request.Context()),
|
||||
RequestID: strings.TrimSpace(ctx.Param("id")),
|
||||
IdempotencyKey: taskKey,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
ctx.Redirect(
|
||||
http.StatusSeeOther,
|
||||
"/freight/"+pathEscape(orderID)+"?notice=task-conflict",
|
||||
)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(http.StatusSeeOther, "/tasks/"+pathEscape(task.ID))
|
||||
}
|
||||
|
||||
func freightNotice(value string) string {
|
||||
switch value {
|
||||
case "request-created":
|
||||
return "采购需求已创建,请补充参考图。"
|
||||
case "request-conflict":
|
||||
return "来源已变化或当前商品不能创建采购需求。"
|
||||
case "image-invalid":
|
||||
return "参考图片无效,请选择 JPG、PNG 或 WebP 后重试。"
|
||||
case "reference-conflict":
|
||||
return "参考图已被使用或来源已变化,请刷新后重试。"
|
||||
case "reference-bound":
|
||||
return "参考图已绑定,可以生成采购任务。"
|
||||
case "task-conflict":
|
||||
return "需求状态或来源已变化,当前不能生成任务。"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func SecurityHeaders() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
ctx.Header(
|
||||
@@ -892,6 +1065,7 @@ type freightImportPage struct {
|
||||
type freightDetailPage struct {
|
||||
Page pageView
|
||||
Detail FreightOrderDetail
|
||||
Notice string
|
||||
}
|
||||
|
||||
type statusOption struct {
|
||||
|
||||
Reference in New Issue
Block a user