普通用户注销账户完整业务逻辑梳理与代码
业务需求说明
- 仅**普通用户(user角色)**可注销,
admin管理员禁止注销账户。 - 注销前置条件:前端必须勾选确认框
confirm_delete_apps:true;防止误操作。 - 注销要级联清理全部关联数据:
- 用户自己上传的所有应用(应用记录、版本、APK静态文件、图标、评论、下载记录)
- 用户提交的全部申诉记录
upload_appeal - 用户自身账户记录
- 注销成功后:该用户JWT令牌直接失效,前端清空本地token,跳转市场首页。
- 保护:注销操作不可逆,永久删除数据。
现有代码现状:
- 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做的事情:
- best‑effort删除apk、图标静态文件(删除失败不阻断数据库)
- 事务内删除:评论、下载记录、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 = "确认注销";
})
}
完整业务执行链路
- 用户打开【账户信息】弹窗 → 点击【注销账户】打开确认弹窗。
- 必须勾选
我确认要删除与我相关的所有应用,确认按钮才可用。 - 点击确认注销,前端提交
{confirm_delete_apps:true}。 - 后端鉴权:
- 如果是admin → 返回不允许注销。
- handler调用
appSvc.DeleteAppsByUploader():- 查询该用户所有应用;逐个调用
DeleteAppFully,清理静态文件、评论、下载记录、版本、应用主表。
- 查询该用户所有应用;逐个调用
- handler调用
userSvc.DeleteUser():- 删除用户全部申诉记录;
- 删除user表用户记录。
- 返回成功;前端清空本地token,强制登出,回到市场首页。
边界安全校验清单
✅1. admin角色完全禁止注销; ✅2. 必须前端传参confirm_delete_apps:true,防止API被恶意调用; ✅3. 用户所有上传的应用全部级联删除,杜绝孤儿无主应用; ✅4. 用户提交过的申诉全部清理; ✅5. 静态apk、图标文件尽力删除(best‑effort,磁盘删除失败不回滚DB); ✅6. 注销成功后前端清除token,旧令牌直接失效