| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- // udeServer: DeleteService 走事务 —— 同一事务内先删该服务下的 t_directory,
- // 再删 t_service。任一步失败全部回滚,不会残留孤儿目录数据。
- package logic
- import (
- "context"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "path"
- "strings"
- "udeServer/internal/audit"
- "udeServer/internal/model"
- "udeServer/internal/svc"
- "udeServer/internal/types"
- "github.com/zeromicro/go-zero/core/logx"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- )
- type DeleteServiceLogic struct {
- logx.Logger
- ctx context.Context
- svcCtx *svc.ServiceContext
- }
- func NewDeleteServiceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteServiceLogic {
- return &DeleteServiceLogic{
- Logger: logx.WithContext(ctx),
- ctx: ctx,
- svcCtx: svcCtx,
- }
- }
- func (l *DeleteServiceLogic) DeleteService(req *types.ServiceIdReq) (*types.DeleteResp, error) {
- if err := checkId(req.Id); err != nil {
- return nil, err
- }
- // 先查出服务实体:删库前要拿 ip/port 去通知外挂程序卸载,删了就查不到了
- svc, err := findService(l.ctx, l.svcCtx.ServiceModel, req.Id)
- if err != nil {
- return nil, err
- }
- // P1 联动卸载:先卸载后删库。卸载失败软处理(不阻塞删除,外挂机器可能已下线),
- // 聚合成 unmountWarning 由前端弹黄色警告。
- unmountWarning := l.notifyFuseUnmount(svc)
- err = l.svcCtx.DB.Transact(func(session sqlx.Session) error {
- dirModel := l.svcCtx.DirectoryModel.WithSession(session)
- svcModel := l.svcCtx.ServiceModel.WithSession(session)
- if err := dirModel.DeleteByServiceId(l.ctx, req.Id); err != nil {
- return fmt.Errorf("delete t_directory of service %d: %w", req.Id, err)
- }
- if err := svcModel.Delete(l.ctx, req.Id); err != nil {
- return fmt.Errorf("delete t_service %d: %w", req.Id, err)
- }
- return nil
- })
- if err != nil {
- l.Errorf("delete service tx failed: %v", err)
- return nil, err
- }
- resp := &types.DeleteResp{Ok: true}
- if unmountWarning != "" {
- resp.UnmountWarning = unmountWarning
- }
- return resp, nil
- }
- // notifyFuseUnmount 删库前回调外挂程序(svc.Ip:svc.Port)的 GET /api/fuseUnMount 接口,
- // 逐目录卸载该服务全部 FUSE 挂载。
- // 前置判断:只有挂载状态 mount_status=mounted 的服务才真正调外挂卸载接口;
- // unmounted/failed 视为无活动挂载,直接跳过不发请求——避免删除"本来就没挂载"
- // 的服务时还要去外挂机器白跑一趟、机器不可达时弹一堆无意义的卸载失败警告。
- // 软处理:单个目录失败只记录并聚合,绝不阻塞删除流程(外挂机器可能已关机,
- // 服务记录必须还能删)。返回空串 = 全部卸载成功或无需卸载。
- func (l *DeleteServiceLogic) notifyFuseUnmount(svc *model.TService) string {
- // MountStatus 是 sql.NullString 类型,.String 取其中的字符串值
- if svc.MountStatus.String != "mounted" {
- return "" // 未挂载/挂载失败,库里明确没有活动挂载,无需通知外挂
- }
- ip, port := svc.Ip.String, svc.Port // port 已是 int64
- if ip == "" || port == 0 {
- return "外挂服务 IP/端口为空,无法联动卸载,目录可能仍处于挂载状态"
- }
- dirs, err := l.svcCtx.DirectoryModel.FindDirsByServiceId(l.ctx, svc.Id)
- if err != nil {
- return fmt.Sprintf("查询保护目录失败,无法联动卸载: %v", err)
- }
- if len(dirs) == 0 {
- return "" // 没配保护目录,无需卸载
- }
- baseURL := fmt.Sprintf("http://%s:%d/api/fuseUnMount", ip, port) // %d:port 是 int64
- var msgs []string
- for _, mount := range dirs {
- // path.Clean(非 filepath!):该路径发给远程 Linux 外挂程序,必须保持 / 分隔符
- cleanMount := path.Clean(mount)
- reqURL, uerr := url.Parse(baseURL)
- if uerr != nil {
- msgs = append(msgs, fmt.Sprintf("解析 URL 失败: %v", uerr))
- continue
- }
- // q.Encode() 自动做 URL 编码(/ → %2F),外挂侧 Query().Get 自动解码还原
- q := reqURL.Query()
- q.Set("mount", cleanMount)
- reqURL.RawQuery = q.Encode()
- l.Infof("fuse unmount forward: GET %s", reqURL.String()) // 打出转发 URL 便于排查
- // DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求(GET 用 url 描述请求原文)
- if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", reqURL.String(),
- audit.BuildRemoteGetRequest(http.MethodGet, reqURL.String()),
- func() (map[string]interface{}, int64, error) {
- // doFuseRequest 注入 X-TOKEN=svc.Iden(接入标识);client.Get 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
- resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodGet, reqURL.String(), nil, "") // 复用挂载逻辑里的 HTTP 客户端(同包变量)
- if rerr != nil {
- return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
- }
- raw, _ := io.ReadAll(resp.Body) // 读到 EOF 保证 TCP 连接复用 Keep-Alive
- resp.Body.Close()
- status := int64(resp.StatusCode)
- if status >= http.StatusBadRequest {
- return nil, status, fmt.Errorf("卸载目录 %s 失败,外挂程序返回 HTTP %d: %s", cleanMount, status, string(raw))
- }
- return map[string]interface{}{"status": status, "body": string(raw)}, status, nil
- }); callErr != nil {
- msgs = append(msgs, callErr.Error())
- continue
- }
- l.Infof("fuse unmount ok: %s", cleanMount)
- }
- if len(msgs) == 0 {
- return ""
- }
- return "服务已删除,但卸载挂载失败: " + strings.Join(msgs, "; ")
- }
|