Version: 0.9.69.dev.260504

后端:
1. 阶段 4 active-scheduler 服务边界落地,新增 `cmd/active-scheduler`、`services/active_scheduler`、`shared/contracts/activescheduler` 和 active-scheduler port,迁移 dry-run、trigger、preview、confirm zrpc 能力
2. active-scheduler outbox consumer、relay、retry loop 和 due job scanner 迁入独立服务入口,gateway `/active-schedule/*` 改为通过 zrpc client 调用
3. gateway 目录收口为 `gateway/api` + `gateway/client`,统一归档 userauth、notification、active-scheduler 的 HTTP 门面和 zrpc client
4. 将旧 `backend/active_scheduler` 领域核心下沉到 `services/active_scheduler/core`,清退旧根目录活跃实现,并补充 active-scheduler 启动期跨域依赖表检查
5. 调整单体启动与 outbox 归属,`cmd/all` 不再启动 active-scheduler workflow、scanner 或 handler

文档:
1. 更新微服务迁移计划,将阶段 4 active-scheduler 标记为首轮收口完成,并明确下一阶段进入 schedule / task / course / task-class
This commit is contained in:
Losita
2026-05-04 21:01:00 +08:00
parent abe3b4960e
commit 4d9a5c4d30
66 changed files with 2048 additions and 466 deletions

View File

@@ -0,0 +1,151 @@
package notification
import (
"errors"
"fmt"
"strings"
"github.com/LoveLosita/smartflow/backend/respond"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// responseFromRPCError 负责把 notification 的 gRPC 错误反解回项目内的 respond.Response。
//
// 职责边界:
// 1. 只在 gateway 边缘层使用,不下沉到服务实现里;
// 2. 业务错误尽量恢复成 respond.Response方便 API 层继续复用现有 DealWithError
// 3. 只要拿不到业务语义,就退化成普通 error让上层按 500 处理。
func responseFromRPCError(err error) error {
if err == nil {
return nil
}
st, ok := status.FromError(err)
if !ok {
return wrapRPCError(err)
}
if resp, ok := responseFromStatus(st); ok {
return resp
}
switch st.Code() {
case codes.Internal, codes.Unknown, codes.Unavailable, codes.DeadlineExceeded, codes.DataLoss, codes.Unimplemented:
msg := strings.TrimSpace(st.Message())
if msg == "" {
msg = "notification zrpc service internal error"
}
return wrapRPCError(errors.New(msg))
}
msg := strings.TrimSpace(st.Message())
if msg == "" {
msg = "notification zrpc service rejected request"
}
return respond.Response{
Status: grpcCodeToRespondStatus(st.Code()),
Info: msg,
}
}
func responseFromStatus(st *status.Status) (respond.Response, bool) {
if st == nil {
return respond.Response{}, false
}
if resp, ok := responseFromStatusDetails(st); ok {
return resp, true
}
if resp, ok := responseFromLegacyStatus(st.Code(), st.Message()); ok {
return resp, true
}
return respond.Response{}, false
}
func responseFromStatusDetails(st *status.Status) (respond.Response, bool) {
for _, detail := range st.Details() {
info, ok := detail.(*errdetails.ErrorInfo)
if !ok {
continue
}
statusValue := strings.TrimSpace(info.Reason)
if statusValue == "" {
statusValue = grpcCodeToRespondStatus(st.Code())
}
if statusValue == "" {
return respond.Response{}, false
}
message := strings.TrimSpace(st.Message())
if message == "" && info.Metadata != nil {
message = strings.TrimSpace(info.Metadata["info"])
}
if message == "" {
message = statusValue
}
return respond.Response{Status: statusValue, Info: message}, true
}
return respond.Response{}, false
}
func responseFromLegacyStatus(code codes.Code, message string) (respond.Response, bool) {
trimmed := strings.TrimSpace(message)
if resp, ok := respondResponseByMessage(trimmed); ok {
return resp, true
}
switch code {
case codes.Unauthenticated:
if trimmed == "" {
trimmed = "unauthorized"
}
return respond.Response{Status: respond.ErrUnauthorized.Status, Info: trimmed}, true
case codes.InvalidArgument:
if trimmed == "" {
trimmed = "invalid argument"
}
return respond.Response{Status: respond.MissingParam.Status, Info: trimmed}, true
case codes.Internal, codes.Unknown, codes.DataLoss:
if trimmed == "" {
trimmed = "notification service internal error"
}
return respond.InternalError(errors.New(trimmed)), true
}
return respond.Response{}, false
}
func respondResponseByMessage(message string) (respond.Response, bool) {
switch strings.TrimSpace(message) {
case respond.MissingParam.Info:
return respond.MissingParam, true
case respond.WrongParamType.Info:
return respond.WrongParamType, true
case respond.ErrUnauthorized.Info:
return respond.ErrUnauthorized, true
}
return respond.Response{}, false
}
func grpcCodeToRespondStatus(code codes.Code) string {
switch code {
case codes.Unauthenticated:
return respond.ErrUnauthorized.Status
case codes.InvalidArgument:
return respond.MissingParam.Status
case codes.Internal, codes.Unknown, codes.DataLoss:
return "500"
default:
return "400"
}
}
func wrapRPCError(err error) error {
if err == nil {
return nil
}
return fmt.Errorf("调用 notification zrpc 服务失败: %w", err)
}