From bdbd6ae7e9799797e3d229e6fc48c78a52120e57 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Mon, 20 Apr 2026 11:34:35 +0800 Subject: [PATCH] f --- app/main/api/desc/front/agent.api | 15 +- app/main/api/desc/front/user.api | 7 +- .../internal/handler/user/cancelouthandler.go | 15 +- .../logic/agent/applyforagentlogic.go | 3 + ...agentsubordinatecontributiondetaillogic.go | 18 +- .../agent/getagentsubordinatelistlogic.go | 41 +- .../api/internal/logic/auth/sendsmslogic.go | 56 +++ .../internal/logic/user/bindmobilelogic.go | 3 + .../api/internal/logic/user/canceloutlogic.go | 295 +++++-------- .../api/internal/logic/user/gettokenlogic.go | 3 + .../logic/user/mobilecodeloginlogic.go | 3 + .../api/internal/logic/user/wxh5authlogic.go | 3 + .../internal/logic/user/wxminiauthlogic.go | 3 + .../userauthinterceptormiddleware.go | 4 + .../api/internal/service/wechatpayService.go | 60 ++- app/main/api/internal/types/types.go | 7 +- app/main/model/userModel_gen.go | 19 +- common/xerr/errCode.go | 1 + common/xerr/errMsg.go | 1 + deploy/script/model/userModel.go | 27 ++ deploy/script/model/userModel_gen.go | 412 ++++++++++++++++++ deploy/sql/user_add_cancelled_at.sql | 3 + 22 files changed, 761 insertions(+), 238 deletions(-) create mode 100644 deploy/script/model/userModel.go create mode 100644 deploy/script/model/userModel_gen.go create mode 100644 deploy/sql/user_add_cancelled_at.sql diff --git a/app/main/api/desc/front/agent.api b/app/main/api/desc/front/agent.api index c3f5e02..52f8b56 100644 --- a/app/main/api/desc/front/agent.api +++ b/app/main/api/desc/front/agent.api @@ -161,13 +161,14 @@ type ( List []AgentSubordinateList `json:"list"` // 查询列表 } AgentSubordinateList { - ID int64 `json:"id"` - Mobile string `json:"mobile"` - CreateTime string `json:"create_time"` - LevelName string `json:"level_name"` - TotalOrders int64 `json:"total_orders"` // 总单量 - TotalEarnings float64 `json:"total_earnings"` // 总金额 - TotalContribution float64 `json:"total_contribution"` // 总贡献 + ID int64 `json:"id"` + Mobile string `json:"mobile"` + CreateTime string `json:"create_time"` + LevelName string `json:"level_name"` + TotalOrders int64 `json:"total_orders"` // 总单量 + TotalEarnings float64 `json:"total_earnings"` // 总金额 + TotalContribution float64 `json:"total_contribution"` // 总贡献 + AccountCancelled bool `json:"account_cancelled"` // 账号是否已注销 } GetAgentSubordinateContributionDetailReq { Page int64 `form:"page"` // 页码 diff --git a/app/main/api/desc/front/user.api b/app/main/api/desc/front/user.api index 8dc69ee..ce41da9 100644 --- a/app/main/api/desc/front/user.api +++ b/app/main/api/desc/front/user.api @@ -110,10 +110,13 @@ service main { post /user/getToken returns (MobileCodeLoginResp) @handler cancelOut - post /user/cancelOut + post /user/cancelOut (CancelOutReq) } type ( + CancelOutReq { + Code string `json:"code" validate:"required"` + } UserInfoResp { UserInfo User `json:"userInfo"` } @@ -143,7 +146,7 @@ type ( sendSmsReq { Mobile string `json:"mobile" validate:"required,mobile"` CaptchaVerifyParam string `json:"captchaVerifyParam,optional,omitempty"` - ActionType string `json:"actionType,optional" validate:"oneof=login register query agentApply realName bindMobile"` + ActionType string `json:"actionType,optional" validate:"oneof=login register query agentApply realName bindMobile cancelAccount"` } ) diff --git a/app/main/api/internal/handler/user/cancelouthandler.go b/app/main/api/internal/handler/user/cancelouthandler.go index 3ebeab2..0a7b398 100644 --- a/app/main/api/internal/handler/user/cancelouthandler.go +++ b/app/main/api/internal/handler/user/cancelouthandler.go @@ -5,13 +5,26 @@ import ( "bdrp-server/app/main/api/internal/logic/user" "bdrp-server/app/main/api/internal/svc" + "bdrp-server/app/main/api/internal/types" "bdrp-server/common/result" + "bdrp-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func CancelOutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + var req types.CancelOutReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } l := user.NewCancelOutLogic(r.Context(), svcCtx) - err := l.CancelOut() + err := l.CancelOut(&req) result.HttpResult(r, w, nil, err) } } diff --git a/app/main/api/internal/logic/agent/applyforagentlogic.go b/app/main/api/internal/logic/agent/applyforagentlogic.go index 7638b67..d67333d 100644 --- a/app/main/api/internal/logic/agent/applyforagentlogic.go +++ b/app/main/api/internal/logic/agent/applyforagentlogic.go @@ -83,6 +83,9 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type if user.Disable == 1 { return errors.Wrapf(xerr.NewErrCode(xerr.USER_DISABLED), "账号已被封禁") } + if user.CancelledAt.Valid { + return errors.Wrapf(xerr.NewErrCode(xerr.USER_CANCELLED), "账号已注销") + } if claims != nil && claims.UserType == model.UserTypeTemp { // 临时用户,转为正式用户 err = l.svcCtx.UserService.TempUserBindUser(l.ctx, session, user.Id) diff --git a/app/main/api/internal/logic/agent/getagentsubordinatecontributiondetaillogic.go b/app/main/api/internal/logic/agent/getagentsubordinatecontributiondetaillogic.go index 03525e6..0b5d2c3 100644 --- a/app/main/api/internal/logic/agent/getagentsubordinatecontributiondetaillogic.go +++ b/app/main/api/internal/logic/agent/getagentsubordinatecontributiondetaillogic.go @@ -161,11 +161,21 @@ func (l *GetAgentSubordinateContributionDetailLogic) GetAgentSubordinateContribu } } - // 解密手机号 + // 解密手机号(账号已注销时展示占位文案) secretKey := l.svcCtx.Config.Encrypt.SecretKey - mobile, err := crypto.DecryptMobile(subordinateAgent.Mobile, secretKey) + subUser, err := l.svcCtx.UserModel.FindOne(l.ctx, subordinateAgent.UserId) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理下级贡献详情, 解密手机号失败: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级贡献详情, 查询用户失败: %v", err) + } + var mobileMasked string + if subUser.CancelledAt.Valid { + mobileMasked = "已注销用户" + } else { + mobile, err := crypto.DecryptMobile(subordinateAgent.Mobile, secretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理下级贡献详情, 解密手机号失败: %v", err) + } + mobileMasked = maskPhone(mobile) } // 获取合并后的分页列表 @@ -187,7 +197,7 @@ func (l *GetAgentSubordinateContributionDetailLogic) GetAgentSubordinateContribu } return &types.GetAgentSubordinateContributionDetailResp{ - Mobile: maskPhone(mobile), + Mobile: mobileMasked, Total: total, CreateTime: closure.CreateTime.Format("2006-01-02 15:04:05"), TotalEarnings: totalEarnings, diff --git a/app/main/api/internal/logic/agent/getagentsubordinatelistlogic.go b/app/main/api/internal/logic/agent/getagentsubordinatelistlogic.go index ae3c4e9..d1155ca 100644 --- a/app/main/api/internal/logic/agent/getagentsubordinatelistlogic.go +++ b/app/main/api/internal/logic/agent/getagentsubordinatelistlogic.go @@ -124,6 +124,20 @@ func (l *GetAgentSubordinateListLogic) GetAgentSubordinateList(req *types.GetAge orderCountMap[v.AgentId]++ } + // 下级对应用户是否已注销 + accountCancelled := make(map[int64]bool) + for _, id := range descendantIDs { + ag := agentMap[id] + if ag == nil { + continue + } + u, err := l.svcCtx.UserModel.FindOne(l.ctx, ag.UserId) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取下级用户信息失败: %v", err) + } + accountCancelled[id] = u.CancelledAt.Valid + } + // 构建返回结果 secretKey := l.svcCtx.Config.Encrypt.SecretKey descendantList = make([]types.AgentSubordinateList, 0, len(descendantIDs)) @@ -133,19 +147,26 @@ func (l *GetAgentSubordinateListLogic) GetAgentSubordinateList(req *types.GetAge continue } - mobile, err := crypto.DecryptMobile(agent.Mobile, secretKey) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理信息, 解密手机号失败: %v", err) + var mobileDisplay string + if accountCancelled[id] { + mobileDisplay = "已注销用户" + } else { + mobile, err := crypto.DecryptMobile(agent.Mobile, secretKey) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理信息, 解密手机号失败: %v", err) + } + mobileDisplay = maskPhone(mobile) } subordinate := types.AgentSubordinateList{ - ID: id, - Mobile: maskPhone(mobile), - LevelName: agent.LevelName, - CreateTime: createTimeMap[id].Format("2006-01-02 15:04:05"), - TotalContribution: deductionMap[id] + rewardsMap[id], - TotalEarnings: commissionMap[id], - TotalOrders: orderCountMap[id], + ID: id, + Mobile: mobileDisplay, + LevelName: agent.LevelName, + CreateTime: createTimeMap[id].Format("2006-01-02 15:04:05"), + TotalContribution: deductionMap[id] + rewardsMap[id], + TotalEarnings: commissionMap[id], + TotalOrders: orderCountMap[id], + AccountCancelled: accountCancelled[id], } descendantList = append(descendantList, subordinate) } diff --git a/app/main/api/internal/logic/auth/sendsmslogic.go b/app/main/api/internal/logic/auth/sendsmslogic.go index b8dda3e..b255762 100644 --- a/app/main/api/internal/logic/auth/sendsmslogic.go +++ b/app/main/api/internal/logic/auth/sendsmslogic.go @@ -3,9 +3,14 @@ package auth import ( "context" "fmt" + "net/http" "math/rand" + "strings" "time" + + "bdrp-server/app/main/model" "bdrp-server/common/xerr" + jwtx "bdrp-server/common/jwt" "bdrp-server/pkg/lzkit/crypto" "github.com/pkg/errors" @@ -53,6 +58,11 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error { if action == "" { action = "login" } + if action == "cancelAccount" { + if err := l.verifyCancelAccountSmsCaller(req); err != nil { + return err + } + } secretKey := l.svcCtx.Config.Encrypt.SecretKey encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) if err != nil { @@ -121,3 +131,49 @@ func (l *SendSmsLogic) sendSmsRequest(mobile, code string) (*dysmsapi.SendSmsRes runtime := &service.RuntimeOptions{} return cli.SendSmsWithOptions(request, runtime) } + +// verifyCancelAccountSmsCaller 注销验证码:须携带与 Jwt 一致的登录态,且手机号与当前账号绑定一致 +func (l *SendSmsLogic) verifyCancelAccountSmsCaller(req *types.SendSmsReq) error { + var authz string + if hr, ok := l.ctx.Value(captcha.HTTPRequestContextKey).(*http.Request); ok && hr != nil { + authz = strings.TrimSpace(hr.Header.Get("Authorization")) + } + if authz == "" { + return errors.Wrapf(xerr.NewErrCode(xerr.TOKEN_EXPIRE_ERROR), "请先登录后再获取注销验证码") + } + tokenStr := strings.TrimSpace(authz) + if len(tokenStr) > 7 && strings.EqualFold(tokenStr[:7], "Bearer ") { + tokenStr = strings.TrimSpace(tokenStr[7:]) + } + claims, err := jwtx.ParseJwtToken(tokenStr, l.svcCtx.Config.JwtAuth.AccessSecret) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.TOKEN_EXPIRE_ERROR), "登录已失效,请重新登录") + } + if claims.UserType != model.UserTypeNormal { + return errors.Wrapf(xerr.NewErrMsg("当前账号类型不支持注销验证码"), "cancelAccount sms") + } + user, err := l.svcCtx.UserModel.FindOne(l.ctx, claims.UserId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.USER_NOT_FOUND), "用户不存在") + } + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败: %v", err) + } + if user.CancelledAt.Valid { + return errors.Wrapf(xerr.NewErrCode(xerr.USER_CANCELLED), "账号已注销") + } + if user.Disable == 1 { + return errors.Wrapf(xerr.NewErrCode(xerr.USER_DISABLED), "账号已被封禁") + } + if !user.Mobile.Valid || user.Mobile.String == "" { + return errors.Wrapf(xerr.NewErrMsg("请先绑定手机号后再注销账号"), "cancelAccount sms, 未绑定手机") + } + enc, err := crypto.EncryptMobile(req.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) + } + if enc != user.Mobile.String { + return errors.Wrapf(xerr.NewErrMsg("手机号与当前登录账号不一致"), "cancelAccount sms mobile mismatch") + } + return nil +} diff --git a/app/main/api/internal/logic/user/bindmobilelogic.go b/app/main/api/internal/logic/user/bindmobilelogic.go index 6d89ec2..1b174ff 100644 --- a/app/main/api/internal/logic/user/bindmobilelogic.go +++ b/app/main/api/internal/logic/user/bindmobilelogic.go @@ -68,6 +68,9 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind if user.Disable == 1 { return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_DISABLED), "账号已被封禁") } + if user.CancelledAt.Valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_CANCELLED), "账号已注销") + } // 进行平台绑定 if claims != nil { if claims.UserType == model.UserTypeTemp { diff --git a/app/main/api/internal/logic/user/canceloutlogic.go b/app/main/api/internal/logic/user/canceloutlogic.go index a31631c..6e7ddb2 100644 --- a/app/main/api/internal/logic/user/canceloutlogic.go +++ b/app/main/api/internal/logic/user/canceloutlogic.go @@ -2,18 +2,23 @@ package user import ( "context" + "database/sql" + "errors" + "fmt" + "os" + "time" + + "bdrp-server/app/main/api/internal/svc" + "bdrp-server/app/main/api/internal/types" "bdrp-server/app/main/model" "bdrp-server/common/ctxdata" "bdrp-server/common/xerr" + "bdrp-server/pkg/lzkit/crypto" - "github.com/zeromicro/go-zero/core/mr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/sqlx" - - "bdrp-server/app/main/api/internal/svc" - + pkgerrors "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type CancelOutLogic struct { @@ -30,222 +35,122 @@ func NewCancelOutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelO } } -func (l *CancelOutLogic) CancelOut() error { - userID, getUserIdErr := ctxdata.GetUidFromCtx(l.ctx) - if getUserIdErr != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", getUserIdErr) +// cancelledMobilePlain 生成唯一占位明文手机号(11 位),用于释放真实手机号唯一约束 +func cancelledMobilePlain(userID int64) string { + return fmt.Sprintf("199%09d", userID%1000000000) +} + +func (l *CancelOutLogic) CancelOut(req *types.CancelOutReq) error { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err) + } + + user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.USER_NOT_FOUND), "用户不存在") + } + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败: %v", err) + } + if user.Disable == 1 { + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.USER_DISABLED), "账号已被封禁") + } + if user.CancelledAt.Valid { + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.USER_CANCELLED), "账号已注销") + } + if !user.Mobile.Valid || user.Mobile.String == "" { + return pkgerrors.Wrapf(xerr.NewErrMsg("请先绑定手机号后再注销账号"), "注销账号, 未绑定手机号 userId=%d", userID) + } + + secretKey := l.svcCtx.Config.Encrypt.SecretKey + encryptedMobile := user.Mobile.String + + if os.Getenv("ENV") != "development" { + codeKey := fmt.Sprintf("%s:%s", "cancelAccount", encryptedMobile) + cacheCode, err := l.svcCtx.Redis.Get(codeKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return pkgerrors.Wrapf(xerr.NewErrMsg("验证码已过期"), "注销账号, 验证码过期") + } + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "注销账号, 读取验证码失败: %v", err) + } + if cacheCode != req.Code { + return pkgerrors.Wrapf(xerr.NewErrMsg("验证码不正确"), "注销账号, 验证码不正确") + } + } + + placeholderPlain := cancelledMobilePlain(userID) + placeholderEnc, err := crypto.EncryptMobile(placeholderPlain, secretKey) + if err != nil { + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "注销账号, 生成占位手机号失败: %v", err) } - // 1. 先检查用户是否是代理 agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询代理信息失败, userId: %d", userID) + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err) } - // 如果用户是代理,进行额外检查 - if agentModel != nil { - // 1.1 检查代理等级是否为VIP或SVIP - if agentModel.LevelName == model.AgentLeveNameVIP || agentModel.LevelName == model.AgentLeveNameSVIP { - return errors.Wrapf(xerr.NewErrMsg("您是"+agentModel.LevelName+"会员,请联系客服进行注销"), "用户是代理会员,不能注销") - } - - // 1.2 检查代理钱包是否有余额或冻结金额 - wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agentModel.Id) - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理钱包失败, agentId: %d", agentModel.Id) - } - - if wallet != nil && (wallet.Balance > 0 || wallet.FrozenBalance > 0) { - if wallet.Balance > 0 { - return errors.Wrapf(xerr.NewErrMsg("您的钱包还有余额%.2f元,请先提现后再注销账号"), "用户钱包有余额,不能注销", wallet.Balance) - } - if wallet.FrozenBalance > 0 { - return errors.Wrapf(xerr.NewErrMsg("您的钱包还有冻结金额%.2f元,请等待解冻后再注销账号"), "用户钱包有冻结金额,不能注销", wallet.FrozenBalance) - } - } - } - - // 在事务中处理用户注销相关操作 err = l.svcCtx.UserModel.Trans(l.ctx, func(tranCtx context.Context, session sqlx.Session) error { - // 1. 删除用户基本信息 - if err := l.svcCtx.UserModel.Delete(tranCtx, session, userID); err != nil { - return errors.Wrapf(err, "删除用户基本信息失败, userId: %d", userID) + dbUser, err := l.svcCtx.UserModel.FindOne(tranCtx, userID) + if err != nil { + return err + } + if dbUser.CancelledAt.Valid { + return xerr.NewErrCode(xerr.USER_CANCELLED) } - // 2. 查询并删除用户授权信息 - UserAuthModelBuilder := l.svcCtx.UserAuthModel.SelectBuilder().Where("user_id = ?", userID) - userAuths, err := l.svcCtx.UserAuthModel.FindAll(tranCtx, UserAuthModelBuilder, "") + dbUser.CancelledAt = sql.NullTime{Time: time.Now(), Valid: true} + dbUser.Mobile = sql.NullString{String: placeholderEnc, Valid: true} + dbUser.Nickname = sql.NullString{String: "已注销用户", Valid: true} + dbUser.Password = sql.NullString{Valid: false} + dbUser.Info = "" + if err := l.svcCtx.UserModel.UpdateWithVersion(tranCtx, session, dbUser); err != nil { + return pkgerrors.Wrapf(err, "更新用户注销状态失败") + } + + authBuilder := l.svcCtx.UserAuthModel.SelectBuilder().Where("user_id = ?", userID) + userAuths, err := l.svcCtx.UserAuthModel.FindAll(tranCtx, authBuilder, "") if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询用户授权信息失败, userId: %d", userID) + return pkgerrors.Wrapf(err, "查询用户授权失败") } - - // 并发删除用户授权信息 - if len(userAuths) > 0 { - funcs := make([]func() error, len(userAuths)) - for i, userAuth := range userAuths { - authID := userAuth.Id - funcs[i] = func() error { - return l.svcCtx.UserAuthModel.Delete(tranCtx, session, authID) - } - } - - if err := mr.Finish(funcs...); err != nil { - return errors.Wrapf(err, "删除用户授权信息失败") + for _, ua := range userAuths { + if err := l.svcCtx.UserAuthModel.Delete(tranCtx, session, ua.Id); err != nil { + return pkgerrors.Wrapf(err, "删除用户授权失败 id=%d", ua.Id) } } - // 3. 处理代理相关信息 - if agentModel != nil { - // 3.1 删除代理信息 - if err := l.svcCtx.AgentModel.Delete(tranCtx, session, agentModel.Id); err != nil { - return errors.Wrapf(err, "删除代理信息失败, agentId: %d", agentModel.Id) - } - - // 3.2 查询并删除代理会员配置 - configBuilder := l.svcCtx.AgentMembershipUserConfigModel.SelectBuilder().Where("agent_id = ?", agentModel.Id) - configs, err := l.svcCtx.AgentMembershipUserConfigModel.FindAll(tranCtx, configBuilder, "") - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询代理会员配置失败, agentId: %d", agentModel.Id) - } - - // 并发删除代理会员配置 - if len(configs) > 0 { - configFuncs := make([]func() error, len(configs)) - for i, config := range configs { - configId := config.Id - configFuncs[i] = func() error { - return l.svcCtx.AgentMembershipUserConfigModel.Delete(tranCtx, session, configId) - } - } - - if err := mr.Finish(configFuncs...); err != nil { - return errors.Wrapf(err, "删除代理会员配置失败") - } - } - - // 3.3 删除代理钱包信息 - wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(tranCtx, agentModel.Id) - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询代理钱包信息失败, agentId: %d", agentModel.Id) - } - - if wallet != nil { - if err := l.svcCtx.AgentWalletModel.Delete(tranCtx, session, wallet.Id); err != nil { - return errors.Wrapf(err, "删除代理钱包信息失败, walletId: %d", wallet.Id) - } - } - - // 3.4 删除代理关系信息 - closureBuilder := l.svcCtx.AgentClosureModel.SelectBuilder().Where("ancestor_id = ? AND depth = ?", agentModel.Id, 1) - closures, err := l.svcCtx.AgentClosureModel.FindAll(tranCtx, closureBuilder, "") - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询代理关系信息失败, agentId: %d", agentModel.Id) - } - - if len(closures) > 0 { - closureFuncs := make([]func() error, len(closures)) - for i, closure := range closures { - closureId := closure.Id - closureFuncs[i] = func() error { - return l.svcCtx.AgentClosureModel.Delete(tranCtx, session, closureId) - } - } - - if err := mr.Finish(closureFuncs...); err != nil { - return errors.Wrapf(err, "删除代理关系信息失败") - } - } - } - - // 4. 查询并删除代理审核信息 - auditBuilder := l.svcCtx.AgentAuditModel.SelectBuilder().Where("user_id = ?", userID) - audits, err := l.svcCtx.AgentAuditModel.FindAll(tranCtx, auditBuilder, "") - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询代理审核信息失败, userId: %d", userID) - } - - // 并发删除代理审核信息 - if len(audits) > 0 { - auditFuncs := make([]func() error, len(audits)) - for i, audit := range audits { - auditId := audit.Id - auditFuncs[i] = func() error { - return l.svcCtx.AgentAuditModel.Delete(tranCtx, session, auditId) - } - } - - if err := mr.Finish(auditFuncs...); err != nil { - return errors.Wrapf(err, "删除代理审核信息失败") - } - } - - // 5. 删除用户查询记录 queryBuilder := l.svcCtx.QueryModel.SelectBuilder().Where("user_id = ?", userID) queries, err := l.svcCtx.QueryModel.FindAll(tranCtx, queryBuilder, "") if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询用户查询记录失败, userId: %d", userID) + return pkgerrors.Wrapf(err, "查询用户查询记录失败") } - - if len(queries) > 0 { - queryFuncs := make([]func() error, len(queries)) - for i, query := range queries { - queryId := query.Id - queryFuncs[i] = func() error { - return l.svcCtx.QueryModel.Delete(tranCtx, session, queryId) - } - } - - if err := mr.Finish(queryFuncs...); err != nil { - return errors.Wrapf(err, "删除用户查询记录失败") + for _, q := range queries { + if err := l.svcCtx.QueryModel.Delete(tranCtx, session, q.Id); err != nil { + return pkgerrors.Wrapf(err, "删除查询记录失败 id=%d", q.Id) } } - // 6. 删除用户订单记录 - orderBuilder := l.svcCtx.OrderModel.SelectBuilder().Where("user_id = ?", userID) - orders, err := l.svcCtx.OrderModel.FindAll(tranCtx, orderBuilder, "") - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询用户订单记录失败, userId: %d", userID) - } - - if len(orders) > 0 { - orderFuncs := make([]func() error, len(orders)) - for i, order := range orders { - orderId := order.Id - orderFuncs[i] = func() error { - return l.svcCtx.OrderModel.Delete(tranCtx, session, orderId) - } + if agentModel != nil { + ag, err := l.svcCtx.AgentModel.FindOne(tranCtx, agentModel.Id) + if err != nil { + return err } - - if err := mr.Finish(orderFuncs...); err != nil { - return errors.Wrapf(err, "删除用户订单记录失败") + ag.Mobile = placeholderEnc + ag.WechatId = sql.NullString{Valid: false} + if err := l.svcCtx.AgentModel.UpdateWithVersion(tranCtx, session, ag); err != nil { + return pkgerrors.Wrapf(err, "更新代理占位信息失败") } } - // 7. 删除代理订单信息 - agentOrderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder().Where("agent_id = ?", agentModel.Id) - agentOrders, err := l.svcCtx.AgentOrderModel.FindAll(tranCtx, agentOrderBuilder, "") - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查询代理订单信息失败, agentId: %d, err: %v", agentModel.Id, err) - } - - if len(agentOrders) > 0 { - agentOrderFuncs := make([]func() error, len(agentOrders)) - for i, agentOrder := range agentOrders { - agentOrderId := agentOrder.Id - agentOrderFuncs[i] = func() error { - return l.svcCtx.AgentOrderModel.Delete(tranCtx, session, agentOrderId) - } - } - - if err := mr.Finish(agentOrderFuncs...); err != nil { - return errors.Wrapf(err, "删除代理订单信息失败") - } - } return nil }) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户注销失败%v", err) + var ce *xerr.CodeError + if errors.As(err, &ce) { + return ce + } + return pkgerrors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户注销失败: %v", err) } return nil diff --git a/app/main/api/internal/logic/user/gettokenlogic.go b/app/main/api/internal/logic/user/gettokenlogic.go index 12e8c46..3bab771 100644 --- a/app/main/api/internal/logic/user/gettokenlogic.go +++ b/app/main/api/internal/logic/user/gettokenlogic.go @@ -50,6 +50,9 @@ func (l *GetTokenLogic) GetToken() (resp *types.MobileCodeLoginResp, err error) if user.Disable == 1 { return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_DISABLED), "账号已被封禁") } + if user.CancelledAt.Valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_CANCELLED), "账号已注销") + } } token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, claims.UserId, claims.UserType) if err != nil { diff --git a/app/main/api/internal/logic/user/mobilecodeloginlogic.go b/app/main/api/internal/logic/user/mobilecodeloginlogic.go index 9ca9918..a96120f 100644 --- a/app/main/api/internal/logic/user/mobilecodeloginlogic.go +++ b/app/main/api/internal/logic/user/mobilecodeloginlogic.go @@ -68,6 +68,9 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r if user.Disable == 1 { return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_DISABLED), "账号已被封禁") } + if user.CancelledAt.Valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_CANCELLED), "账号已注销") + } userID = user.Id } token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) diff --git a/app/main/api/internal/logic/user/wxh5authlogic.go b/app/main/api/internal/logic/user/wxh5authlogic.go index 26f9f36..c742858 100644 --- a/app/main/api/internal/logic/user/wxh5authlogic.go +++ b/app/main/api/internal/logic/user/wxh5authlogic.go @@ -57,6 +57,9 @@ func (l *WxH5AuthLogic) WxH5Auth(req *types.WXH5AuthReq) (resp *types.WXH5AuthRe if user.Disable == 1 { return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_DISABLED), "账号已被封禁") } + if user.CancelledAt.Valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_CANCELLED), "账号已注销") + } userID = userAuth.UserId userType = model.UserTypeNormal } else { diff --git a/app/main/api/internal/logic/user/wxminiauthlogic.go b/app/main/api/internal/logic/user/wxminiauthlogic.go index 4045bfe..59741ae 100644 --- a/app/main/api/internal/logic/user/wxminiauthlogic.go +++ b/app/main/api/internal/logic/user/wxminiauthlogic.go @@ -56,6 +56,9 @@ func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMi if user.Disable == 1 { return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_DISABLED), "账号已被封禁") } + if user.CancelledAt.Valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_CANCELLED), "账号已注销") + } userID = userAuth.UserId userType = model.UserTypeNormal } else { diff --git a/app/main/api/internal/middleware/userauthinterceptormiddleware.go b/app/main/api/internal/middleware/userauthinterceptormiddleware.go index b6d20c1..6f20948 100644 --- a/app/main/api/internal/middleware/userauthinterceptormiddleware.go +++ b/app/main/api/internal/middleware/userauthinterceptormiddleware.go @@ -44,6 +44,10 @@ func (m *UserAuthInterceptorMiddleware) Handle(next http.HandlerFunc) http.Handl m.writeErrorResponse(w, http.StatusForbidden, xerr.NewErrCode(xerr.USER_DISABLED)) return } + if user.CancelledAt.Valid { + m.writeErrorResponse(w, http.StatusForbidden, xerr.NewErrCode(xerr.USER_CANCELLED)) + return + } next(w, r) } } diff --git a/app/main/api/internal/service/wechatpayService.go b/app/main/api/internal/service/wechatpayService.go index a84fdad..f580a9c 100644 --- a/app/main/api/internal/service/wechatpayService.go +++ b/app/main/api/internal/service/wechatpayService.go @@ -2,10 +2,16 @@ package service import ( "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" "fmt" "net/http" "strconv" "time" + "bdrp-server/app/main/api/internal/config" "bdrp-server/app/main/model" "bdrp-server/common/ctxdata" @@ -151,11 +157,23 @@ func newWechatPayServiceWithWxPayPubKey(c config.Config, userAuthModel model.Use } } -// CreateWechatAppOrder 创建微信APP支付订单 -func (w *WechatPayService) CreateWechatAppOrder(ctx context.Context, amount float64, description string, outTradeNo string) (string, error) { +// randomNonceWechat 生成微信调起支付用的随机串 +func randomNonceWechat(n int) (string, error) { + const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return "", err + } + for i := range b { + b[i] = letters[int(b[i])%len(letters)] + } + return string(b), nil +} + +// CreateWechatAppOrder 创建微信 APP 支付:统一下单后按 V3「APP 调起支付」规则签名,供原生 SDK / uni.requestPayment(wxpay) 使用 +func (w *WechatPayService) CreateWechatAppOrder(ctx context.Context, amount float64, description string, outTradeNo string) (map[string]string, error) { totalAmount := lzUtils.ToWechatAmount(amount) - // 构建支付请求参数 payRequest := app.PrepayRequest{ Appid: core.String(w.config.Wxpay.AppID), Mchid: core.String(w.config.Wxpay.MchID), @@ -167,17 +185,41 @@ func (w *WechatPayService) CreateWechatAppOrder(ctx context.Context, amount floa }, } - // 初始化 AppApiService svc := app.AppApiService{Client: w.wechatClient} - - // 发起预支付请求 resp, result, err := svc.Prepay(ctx, payRequest) if err != nil { - return "", fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode) + return nil, fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode) } - // 返回预支付交易会话标识 - return *resp.PrepayId, nil + prepayID := *resp.PrepayId + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(w.config.Wxpay.MchPrivateKeyPath) + if err != nil { + return nil, fmt.Errorf("加载商户私钥失败: %w", err) + } + + ts := strconv.FormatInt(time.Now().Unix(), 10) + nonceStr, err := randomNonceWechat(32) + if err != nil { + return nil, err + } + + appID := w.config.Wxpay.AppID + msg := fmt.Sprintf("%s\n%s\n%s\n%s\n", appID, ts, nonceStr, prepayID) + sum := sha256.Sum256([]byte(msg)) + sig, err := rsa.SignPKCS1v15(rand.Reader, mchPrivateKey, crypto.SHA256, sum[:]) + if err != nil { + return nil, fmt.Errorf("APP 调起支付签名失败: %w", err) + } + + return map[string]string{ + "appid": appID, + "partnerid": w.config.Wxpay.MchID, + "prepayid": prepayID, + "package": "Sign=WXPay", + "noncestr": nonceStr, + "timestamp": ts, + "sign": base64.StdEncoding.EncodeToString(sig), + }, nil } // CreateWechatMiniProgramOrder 创建微信小程序支付订单 diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index 9d902c4..136fccf 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -1282,6 +1282,7 @@ type AgentSubordinateList struct { TotalOrders int64 `json:"total_orders"` // 总单量 TotalEarnings float64 `json:"total_earnings"` // 总金额 TotalContribution float64 `json:"total_contribution"` // 总贡献 + AccountCancelled bool `json:"account_cancelled"` // 账号是否已注销 } type AgentWalletTransactionListItem struct { @@ -1346,6 +1347,10 @@ type BindMobileResp struct { RefreshAfter int64 `json:"refreshAfter"` } +type CancelOutReq struct { + Code string `json:"code" validate:"required"` +} + type Commission struct { OrderId string `json:"order_id"` // 订单号 ProductName string `json:"product_name"` @@ -2231,5 +2236,5 @@ type GetAppVersionResp struct { type SendSmsReq struct { Mobile string `json:"mobile" validate:"required,mobile"` CaptchaVerifyParam string `json:"captchaVerifyParam,optional,omitempty"` - ActionType string `json:"actionType,optional" validate:"oneof=login register query agentApply realName bindMobile"` + ActionType string `json:"actionType,optional" validate:"oneof=login register query agentApply realName bindMobile cancelAccount"` } diff --git a/app/main/model/userModel_gen.go b/app/main/model/userModel_gen.go index 2654ed8..c219599 100644 --- a/app/main/model/userModel_gen.go +++ b/app/main/model/userModel_gen.go @@ -67,8 +67,9 @@ type ( Password sql.NullString `db:"password"` Nickname sql.NullString `db:"nickname"` Info string `db:"info"` - Inside int64 `db:"inside"` - Disable int64 `db:"disable"` // 0可用 1禁用 + Inside int64 `db:"inside"` + Disable int64 `db:"disable"` // 0可用 1禁用 + CancelledAt sql.NullTime `db:"cancelled_at"` // 账号注销时间,未注销为 NULL } ) @@ -84,11 +85,11 @@ func (m *defaultUserModel) Insert(ctx context.Context, session sqlx.Session, dat bdrpUserIdKey := fmt.Sprintf("%s%v", cacheBdrpUserIdPrefix, data.Id) bdrpUserMobileKey := fmt.Sprintf("%s%v", cacheBdrpUserMobilePrefix, data.Mobile) return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet) + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet) if session != nil { - return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Mobile, data.Password, data.Nickname, data.Info, data.Inside, data.Disable) + return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Mobile, data.Password, data.Nickname, data.Info, data.Inside, data.Disable, data.CancelledAt) } - return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Mobile, data.Password, data.Nickname, data.Info, data.Inside, data.Disable) + return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Mobile, data.Password, data.Nickname, data.Info, data.Inside, data.Disable, data.CancelledAt) }, bdrpUserIdKey, bdrpUserMobileKey) } @@ -139,9 +140,9 @@ func (m *defaultUserModel) Update(ctx context.Context, session sqlx.Session, new return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userRowsWithPlaceHolder) if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.Id) + return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.CancelledAt, newData.Id) } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.Id) + return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.CancelledAt, newData.Id) }, bdrpUserIdKey, bdrpUserMobileKey) } @@ -162,9 +163,9 @@ func (m *defaultUserModel) UpdateWithVersion(ctx context.Context, session sqlx.S sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, userRowsWithPlaceHolder) if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.Id, oldVersion) + return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.CancelledAt, newData.Id, oldVersion) } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.Id, oldVersion) + return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.CancelledAt, newData.Id, oldVersion) }, bdrpUserIdKey, bdrpUserMobileKey) if err != nil { return err diff --git a/common/xerr/errCode.go b/common/xerr/errCode.go index b212fde..bc550b9 100644 --- a/common/xerr/errCode.go +++ b/common/xerr/errCode.go @@ -18,6 +18,7 @@ const USER_NOT_FOUND uint32 = 100009 const USER_NEED_BIND_MOBILE uint32 = 100010 const USER_DISABLED uint32 = 100011 // 账号已被封禁 const USER_TEMP_INVALID uint32 = 100012 +const USER_CANCELLED uint32 = 100013 // 账号已注销 const LOGIN_FAILED uint32 = 200001 const LOGIC_QUERY_WAIT uint32 = 200002 diff --git a/common/xerr/errMsg.go b/common/xerr/errMsg.go index 975252b..e7bcafd 100644 --- a/common/xerr/errMsg.go +++ b/common/xerr/errMsg.go @@ -15,6 +15,7 @@ func init() { message[USER_NEED_BIND_MOBILE] = "请先绑定手机号" message[USER_DISABLED] = "账号已被封禁" message[USER_TEMP_INVALID] = "用户状态异常" + message[USER_CANCELLED] = "账号已注销" } func MapErrMsg(errcode uint32) string { diff --git a/deploy/script/model/userModel.go b/deploy/script/model/userModel.go new file mode 100644 index 0000000..8123712 --- /dev/null +++ b/deploy/script/model/userModel.go @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ UserModel = (*customUserModel)(nil) + +type ( + // UserModel is an interface to be customized, add more methods here, + // and implement the added methods in customUserModel. + UserModel interface { + userModel + } + + customUserModel struct { + *defaultUserModel + } +) + +// NewUserModel returns a model for the database table. +func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) UserModel { + return &customUserModel{ + defaultUserModel: newUserModel(conn, c), + } +} diff --git a/deploy/script/model/userModel_gen.go b/deploy/script/model/userModel_gen.go new file mode 100644 index 0000000..01feb42 --- /dev/null +++ b/deploy/script/model/userModel_gen.go @@ -0,0 +1,412 @@ +// Code generated by goctl. DO NOT EDIT! + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "time" + + "bdrp-server/common/globalkey" + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + userFieldNames = builder.RawFieldNames(&User{}) + userRows = strings.Join(userFieldNames, ",") + userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_time`", "`update_time`"), ",") + userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" + + cacheBdrpUserIdPrefix = "cache:bdrp:user:id:" + cacheBdrpUserMobilePrefix = "cache:bdrp:user:mobile:" +) + +type ( + userModel interface { + Insert(ctx context.Context, session sqlx.Session, data *User) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*User, error) + FindOneByMobile(ctx context.Context, mobile sql.NullString) (*User, error) + Update(ctx context.Context, session sqlx.Session, data *User) (sql.Result, error) + UpdateWithVersion(ctx context.Context, session sqlx.Session, data *User) error + Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error + SelectBuilder() squirrel.SelectBuilder + DeleteSoft(ctx context.Context, session sqlx.Session, data *User) error + FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) + FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) + FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*User, error) + FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*User, error) + FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*User, int64, error) + FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*User, error) + FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*User, error) + Delete(ctx context.Context, session sqlx.Session, id int64) error + } + + defaultUserModel struct { + sqlc.CachedConn + table string + } + + User struct { + Id int64 `db:"id"` + CreateTime time.Time `db:"create_time"` + UpdateTime time.Time `db:"update_time"` + DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 + DelState int64 `db:"del_state"` + Version int64 `db:"version"` // 版本号 + Mobile sql.NullString `db:"mobile"` + Password sql.NullString `db:"password"` + Nickname sql.NullString `db:"nickname"` + Info string `db:"info"` + Inside int64 `db:"inside"` + Disable int64 `db:"disable"` // 0可用 1禁用 + CancelledAt sql.NullTime `db:"cancelled_at"` // 账号注销时间 + } +) + +func newUserModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultUserModel { + return &defaultUserModel{ + CachedConn: sqlc.NewConn(conn, c), + table: "`user`", + } +} + +func (m *defaultUserModel) Insert(ctx context.Context, session sqlx.Session, data *User) (sql.Result, error) { + data.DelState = globalkey.DelStateNo + bdrpUserIdKey := fmt.Sprintf("%s%v", cacheBdrpUserIdPrefix, data.Id) + bdrpUserMobileKey := fmt.Sprintf("%s%v", cacheBdrpUserMobilePrefix, data.Mobile) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet) + if session != nil { + return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Mobile, data.Password, data.Nickname, data.Info, data.Inside, data.Disable, data.CancelledAt) + } + return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Mobile, data.Password, data.Nickname, data.Info, data.Inside, data.Disable, data.CancelledAt) + }, bdrpUserIdKey, bdrpUserMobileKey) +} + +func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error) { + bdrpUserIdKey := fmt.Sprintf("%s%v", cacheBdrpUserIdPrefix, id) + var resp User + err := m.QueryRowCtx(ctx, &resp, bdrpUserIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", userRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultUserModel) FindOneByMobile(ctx context.Context, mobile sql.NullString) (*User, error) { + bdrpUserMobileKey := fmt.Sprintf("%s%v", cacheBdrpUserMobilePrefix, mobile) + var resp User + err := m.QueryRowIndexCtx(ctx, &resp, bdrpUserMobileKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { + query := fmt.Sprintf("select %s from %s where `mobile` = ? and del_state = ? limit 1", userRows, m.table) + if err := conn.QueryRowCtx(ctx, &resp, query, mobile, globalkey.DelStateNo); err != nil { + return nil, err + } + return resp.Id, nil + }, m.queryPrimary) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultUserModel) Update(ctx context.Context, session sqlx.Session, newData *User) (sql.Result, error) { + data, err := m.FindOne(ctx, newData.Id) + if err != nil { + return nil, err + } + bdrpUserIdKey := fmt.Sprintf("%s%v", cacheBdrpUserIdPrefix, data.Id) + bdrpUserMobileKey := fmt.Sprintf("%s%v", cacheBdrpUserMobilePrefix, data.Mobile) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.CancelledAt, newData.Id) + } + return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.CancelledAt, newData.Id) + }, bdrpUserIdKey, bdrpUserMobileKey) +} + +func (m *defaultUserModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *User) error { + + oldVersion := newData.Version + newData.Version += 1 + + var sqlResult sql.Result + var err error + + data, err := m.FindOne(ctx, newData.Id) + if err != nil { + return err + } + bdrpUserIdKey := fmt.Sprintf("%s%v", cacheBdrpUserIdPrefix, data.Id) + bdrpUserMobileKey := fmt.Sprintf("%s%v", cacheBdrpUserMobilePrefix, data.Mobile) + sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, userRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.CancelledAt, newData.Id, oldVersion) + } + return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Inside, newData.Disable, newData.CancelledAt, newData.Id, oldVersion) + }, bdrpUserIdKey, bdrpUserMobileKey) + if err != nil { + return err + } + updateCount, err := sqlResult.RowsAffected() + if err != nil { + return err + } + if updateCount == 0 { + return ErrNoRowsUpdate + } + + return nil +} + +func (m *defaultUserModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *User) error { + data.DelState = globalkey.DelStateYes + data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} + if err := m.UpdateWithVersion(ctx, session, data); err != nil { + return errors.Wrapf(errors.New("delete soft failed "), "UserModel delete err : %+v", err) + } + return nil +} + +func (m *defaultUserModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") + } + + builder = builder.Columns("IFNULL(SUM(" + field + "),0)") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp float64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultUserModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") + } + + builder = builder.Columns("COUNT(" + field + ")") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp int64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultUserModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*User, error) { + + builder = builder.Columns(userRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return nil, err + } + + var resp []*User + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultUserModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*User, error) { + + builder = builder.Columns(userRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*User + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultUserModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*User, int64, error) { + + total, err := m.FindCount(ctx, builder, "id") + if err != nil { + return nil, 0, err + } + + builder = builder.Columns(userRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, total, err + } + + var resp []*User + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, total, nil + default: + return nil, total, err + } +} + +func (m *defaultUserModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*User, error) { + + builder = builder.Columns(userRows) + + if preMinId > 0 { + builder = builder.Where(" id < ? ", preMinId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*User + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultUserModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*User, error) { + + builder = builder.Columns(userRows) + + if preMaxId > 0 { + builder = builder.Where(" id > ? ", preMaxId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*User + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultUserModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { + + return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { + return fn(ctx, session) + }) + +} + +func (m *defaultUserModel) SelectBuilder() squirrel.SelectBuilder { + return squirrel.Select().From(m.table) +} +func (m *defaultUserModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + return err + } + + bdrpUserIdKey := fmt.Sprintf("%s%v", cacheBdrpUserIdPrefix, id) + bdrpUserMobileKey := fmt.Sprintf("%s%v", cacheBdrpUserMobilePrefix, data.Mobile) + _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + if session != nil { + return session.ExecCtx(ctx, query, id) + } + return conn.ExecCtx(ctx, query, id) + }, bdrpUserIdKey, bdrpUserMobileKey) + return err +} +func (m *defaultUserModel) formatPrimary(primary interface{}) string { + return fmt.Sprintf("%s%v", cacheBdrpUserIdPrefix, primary) +} +func (m *defaultUserModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", userRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) +} + +func (m *defaultUserModel) tableName() string { + return m.table +} diff --git a/deploy/sql/user_add_cancelled_at.sql b/deploy/sql/user_add_cancelled_at.sql new file mode 100644 index 0000000..a4d3289 --- /dev/null +++ b/deploy/sql/user_add_cancelled_at.sql @@ -0,0 +1,3 @@ +-- 账号注销:标记注销时间(NULL 表示未注销) +ALTER TABLE `user` + ADD COLUMN `cancelled_at` DATETIME NULL DEFAULT NULL COMMENT '账号注销时间' AFTER `disable`;