应用市场V1.0 普通用户 注销账户的业务逻辑

普通用户注销账户完整业务逻辑梳理与代码

业务需求说明

  1. 仅**普通用户(user角色)**可注销,admin管理员禁止注销账户
  2. 注销前置条件:前端必须勾选确认框 confirm_delete_apps:true;防止误操作。
  3. 注销要级联清理全部关联数据:
    • 用户自己上传的所有应用(应用记录、版本、APK静态文件、图标、评论、下载记录)
    • 用户提交的全部申诉记录 upload_appeal
    • 用户自身账户记录
  4. 注销成功后:该用户JWT令牌直接失效,前端清空本地token,跳转市场首页
  5. 保护:注销操作不可逆,永久删除数据。

现有代码现状:

  • handler/auth.go 已有 Deregister 接口;
  • 之前做过重构:把删除应用逻辑从UserService移到Handler层,避免service循环依赖

1、service/user.go 【UserService.DeleteUser】

Service层职责:只做两件:①删除该用户全部申诉记录;②删除user表用户本身。不处理应用删除,应用删除交给handler调用AppService

复制代码
// DeleteUser 删除普通用户账户
// 注意:应用删除逻辑放在handler层调用 appSvc.DeleteAppsByUploader,解除循环依赖
func (s *UserService) DeleteUser(userID uint) error {
	// 校验用户是否真实存在
	u, err := s.userRepo.FindUserByID(userID)
	if err != nil {
		return errors.New("用户不存在")
	}
	// 1、删除该用户全部申诉记录
	if err := s.appealRepo.DeleteByUser(userID); err != nil {
		return errors.New("清理用户申诉记录失败")
	}
	// 2、删除user表该行记录
	if err := s.userRepo.Delete(userID); err != nil {
		return errors.New("删除用户账户失败")
	}
	return nil
}

2、handler/auth.go Deregister 注销接口(完整)

handler层:鉴权 → 判断是普通用户 → 读取确认参数 → 调用appSvc删除该用户全部应用 → 调用userSvc删除用户+申诉。

复制代码
// Deregister 用户注销账户(需 LoginAuth:仅 user 有意义)
// POST /api/v1/auth/deregister
// Body: {"confirm_delete_apps":true}
func (h *AuthHandler) Deregister(c *gin.Context) {
	var req struct {
		ConfirmDeleteApps bool `json:"confirm_delete_apps"`
	}
	_ = c.ShouldBindJSON(&req)

	roleStr, uid := actorFromContext(c)
	// 管理员禁止注销
	if roleStr != util.RoleUser {
		util.Fail(c, "管理员账户不支持注销")
		return
	}
	// 必须勾选确认删除全部应用
	if !req.ConfirmDeleteApps {
		util.Fail(c, "注销账户须确认删除与其相关的所有应用,请勾选确认")
		return
	}

	up := service.Uploader{
		Type: util.RoleUser,
		ID:   uid,
	}
	// 第一步:删除该用户上传的全部应用(含版本、评论、下载日志、静态apk/icon文件)
	delCount, err := h.appSvc.DeleteAppsByUploader(up)
	if err != nil {
		util.Fail(c, "删除用户关联应用失败:"+err.Error())
		return
	}

	// 第二步:删除用户申诉记录 + 删除user账户
	err = h.userSvc.DeleteUser(uid)
	if err != nil {
		util.Fail(c, "注销账户失败:"+err.Error())
		return
	}

	util.SuccessMsg(c, "账户已注销,已删除"+strconv.Itoa(delCount)+"个关联应用")
}

⚠️重要:AuthHandler结构体必须注入AppService

打开 handler/auth.go 修改结构体、构造函数,注入appSvc,否则调用h.appSvc.DeleteAppsByUploader会报错。

复制代码
type AuthHandler struct {
	userSvc *service.UserService
	appSvc  *service.AppService // ✅新增注入AppService
}

func NewAuthHandler(userSvc *service.UserService, appSvc *service.AppService) *AuthHandler {
	return &AuthHandler{
		userSvc: userSvc,
		appSvc:  appSvc, // ✅赋值
	}
}

main.go 初始化路由/handler处传参

找到main.go创建AuthHandler:

复制代码
// 原来
// authHandler := handler.NewAuthHandler(userSvc)

// 修改为,传入 appSvc
authHandler := handler.NewAuthHandler(userSvc, appSvc)

3、AppService.DeleteAppsByUploader(service/app.go,已有)

复制代码
// DeleteAppsByUploader 删除指定上传者的全部应用,静态文件best‑effort清理
// 返回删除应用数量,首个错误
func (s *AppService) DeleteAppsByUploader(up Uploader) (int, error) {
	apps, err := s.appRepo.FindByUploader(up.Type, up.ID)
	if err != nil {
		return 0, errors.New("查询该用户上传应用失败")
	}
	count := 0
	var firstErr error
	for _, app := range apps {
		e := s.DeleteAppFully(app.ID)
		if e != nil {
			if firstErr == nil {
				firstErr = e
			}
			continue
		}
		count++
	}
	return count, firstErr
}

DeleteAppFully做的事情:

  1. best‑effort删除apk、图标静态文件(删除失败不阻断数据库)
  2. 事务内删除:评论、下载记录、app_version、app_info。

4、前端逻辑(index.html + main.js)

index.html 弹窗(已经存在)

复制代码
<!-- 注销确认弹层 -->
<div class="modal-mask" id="deregisterModal">
    <div class="modal">
        <h2>注销账户确认</h2>
        <div class="hint" style="margin-bottom:10px;color:#f56c6c;">警告:注销将永久删除您的账户及其全部应用(含版本/评论/下载记录),不可恢复!</div>
        <div class="field" style="background:#fff8e6;padding:10px;border-radius:4px;">
            <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
            <input type="checkbox" id="dgConfirm" onchange="toggleDeregisterBtn()" />
            <span>我确认要删除与我相关的所有应用</span>
        </label>
        </div>
        <div class="hint" style="margin-top:6px;">必须勾选确认才可注销,注销后不会出现无主应用。</div>
        <div class="footer">
            <button class="btn-default" onclick="closeModal('deregisterModal')">取消</button>
            <button class="btn-danger" id="dgBtn" onclick="doDeregister()" disabled>确认注销</button>
        </div>
    </div>
</div>

main.js 前端注销逻辑

复制代码
function openDeregisterConfirm() {
    document.getElementById('dgConfirm').checked = false;
    document.getElementById('dgBtn').disabled = true;
    closeModal('profileModal');
    openModal('deregisterModal');
}
function toggleDeregisterBtn() {
    document.getElementById('dgBtn').disabled = !document.getElementById('dgConfirm').checked;
}

// 执行注销
function doDeregister() {
    var btn = document.getElementById('dgBtn');
    btn.disabled = true;
    btn.textContent = "注销中...";
    authFetch(API.deregister, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            confirm_delete_apps: true
        })
    }).then(function (res) {
        if (res.code === 0) {
            toast('账户已注销', 'ok');
            closeModal('deregisterModal');
            // ✅注销成功,清空本地token,强制登出,跳转到市场首页
            clearAuth();
            renderTopbar();
            goMarket();
        } else {
            toast(res.msg || '注销失败', 'err');
        }
    }).catch(function () {
        toast("网络异常", "err");
    }).then(function () {
        btn.disabled = false;
        btn.textContent = "确认注销";
    })
}

完整业务执行链路

  1. 用户打开【账户信息】弹窗 → 点击【注销账户】打开确认弹窗。
  2. 必须勾选我确认要删除与我相关的所有应用,确认按钮才可用。
  3. 点击确认注销,前端提交{confirm_delete_apps:true}
  4. 后端鉴权:
    • 如果是admin → 返回不允许注销。
  5. handler调用 appSvc.DeleteAppsByUploader()
    • 查询该用户所有应用;逐个调用DeleteAppFully,清理静态文件、评论、下载记录、版本、应用主表。
  6. handler调用userSvc.DeleteUser()
    • 删除用户全部申诉记录;
    • 删除user表用户记录。
  7. 返回成功;前端清空本地token,强制登出,回到市场首页

边界安全校验清单

✅1. admin角色完全禁止注销; ✅2. 必须前端传参confirm_delete_apps:true,防止API被恶意调用; ✅3. 用户所有上传的应用全部级联删除,杜绝孤儿无主应用; ✅4. 用户提交过的申诉全部清理; ✅5. 静态apk、图标文件尽力删除(best‑effort,磁盘删除失败不回滚DB); ✅6. 注销成功后前端清除token,旧令牌直接失效

相关推荐
咏方舟【长江支流】2 小时前
【前端2】单据编辑 -订单单据主子表,万能模板行,自定义添加行
前端·前端框架·状态模式·咏方舟-长江支流
涤生大数据11 小时前
一次“没有运行日志”的DolphinScheduler任务失败排查
大数据·数据库·人工智能·状态模式
FungLeo1 天前
React 管理后台实战 · 列表里突然出现一堆空白行?PIPL 擦除后前端怎么处理才不露馅
前端·react.js·状态模式·pipl
国科安芯1 天前
ASL3159S:把精密信号的“破音“挡在切换之外的 0.9Ω 开关
服务器·网络·数据库·架构·状态模式·抗辐射加固
不灭的程序员阿澄2 天前
Nginx 容器化部署实战:SPA 路由、Cookie 透传与 WebSocket 全配置详解
websocket·nginx·状态模式
国科安芯4 天前
ASL622S:把“信号失真“挡在门外的航天级运放
网络·单片机·嵌入式硬件·架构·状态模式·低轨卫星星座
leoZ2318 天前
Vue3 还原一个企业级后台-08-API注册管理
目标检测·机器学习·分类·状态模式·迁移学习·集成学习·figma
Java&Develop9 天前
QuantDinger 宝塔 Linux 部署完整指南
linux·运维·状态模式