应用市场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,旧令牌直接失效

相关推荐
JianZhen✓12 小时前
解决SPA发版旧版本残留+动态路由打包失效的完整方案(附落地代码)
前端·状态模式
2501_933923251 天前
统一功能:统一返回格式+统一异常处理
spring·java-ee·状态模式·统一数据返回
小肥君2 天前
前端测试websocket
前端·websocket·状态模式
Wang's Blog2 天前
Java框架快速入门: Spring Security+OAuth2之前后端分离多模块工程配置
java·spring·状态模式
weixin_493503674 天前
Vue3 前端生成 PDF:会员证书与活动签到表的三种打印方案与踩坑记录
前端·pdf·状态模式
晴天164 天前
前端跨域方案解析:JSONP 的原理、实战与演进
前端·状态模式
晴天165 天前
HTML iframe 标签全面解析:原理、属性、实战场景与最佳实践
前端·html·状态模式
nvd115 天前
深入现代 Web 鉴权架构:网关统一代理 (Forward-Auth) vs 前端持有 JWT 的终极选型与边缘同域实践
前端·架构·状态模式
你别说话了5 天前
Vue3对接AI Agent: 封装流式对话组件
状态模式