package logic import ( "context" "encoding/json" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/guid" "golang.org/x/crypto/bcrypt" v1 "tool-api/api/user/v1" "tool-api/internal/consts" "tool-api/internal/model/entity" ) // WxLogin 微信登录(code 换 openid) func WxLogin(ctx context.Context, code string) (*v1.WxLoginRes, error) { session, err := code2Session(ctx, code) if err != nil { return nil, err } return loginByOpenid(ctx, session.Openid, session.SessionKey) } // DevLogin 开发联调登录(仅 debug 配置开放) func DevLogin(ctx context.Context) (*v1.WxLoginRes, error) { if !IsDebug(ctx) { return nil, gerror.New("当前环境未开启 debug,禁止开发登录") } return loginByOpenid(ctx, "dev-user", "") } // UserLevel 我的等级与已授权模块(已过期回落默认等级) func UserLevel(ctx context.Context) (*v1.LevelRes, error) { user, err := getUserById(ctx, CtxUserId(ctx)) if err != nil { return nil, err } return EffectiveLevelInfo(ctx, user) } // GetUserInfo 当前登录用户信息 func GetUserInfo(ctx context.Context) (*v1.UserInfo, error) { user, err := getUserById(ctx, CtxUserId(ctx)) if err != nil { return nil, err } return &v1.UserInfo{Id: user.Id, Nickname: user.Nickname, AvatarUrl: user.AvatarUrl}, nil } // wxSession jscode2session 返回:openid + session_key type wxSession struct { Openid string `json:"openid"` SessionKey string `json:"session_key"` ErrCode int `json:"errcode"` ErrMsg string `json:"errmsg"` } // code2Session code 换 openid + session_key。 // session_key 是虚拟支付「用户态签名 signature」的密钥,必须与 openid 一起取回。 func code2Session(ctx context.Context, code string) (*wxSession, error) { // 联调快捷通道:debug 模式下 code 传 "dev" if code == "dev" && IsDebug(ctx) { return &wxSession{Openid: "dev-user"}, nil } appId := g.Cfg().MustGet(ctx, "wx.appId").String() secret := g.Cfg().MustGet(ctx, "wx.appSecret").String() if appId == "" || secret == "" { return nil, gerror.New("服务端未配置微信 appId/appSecret,请填写 manifest/config/config.yaml 后重启") } url := fmt.Sprintf( "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appId, secret, code, ) resp, err := g.Client().Get(ctx, url) if err != nil { return nil, gerror.Newf("请求微信接口失败: %v", err) } defer resp.Close() out := &wxSession{} if err = json.Unmarshal(resp.ReadAll(), out); err != nil { return nil, gerror.Newf("解析微信响应失败: %v", err) } if out.ErrCode != 0 || out.Openid == "" { return nil, gerror.Newf("微信登录失败: %d %s", out.ErrCode, out.ErrMsg) } return out, nil } func resolveOpenid(ctx context.Context, code string) (string, error) { session, err := code2Session(ctx, code) if err != nil { return "", err } return session.Openid, nil } // RefreshSessionKey 换新 session_key 并落库(虚拟支付下单前调用)。 // 返回该 code 对应的 openid,供调用方校验「支付人 = 登录人」。 func RefreshSessionKey(ctx context.Context, userId int64, code string) (string, error) { if code == "" { // 未传 code:复用已落库的 session_key(可能已过期,支付时由平台判失败) user, err := getUserById(ctx, userId) if err != nil { return "", err } return user.Openid, nil } session, err := code2Session(ctx, code) if err != nil { return "", err } if _, err = g.Model(consts.TableUsers).Where("id", userId).Data(g.Map{ "session_key": session.SessionKey, }).Update(); err != nil { return "", err } return session.Openid, nil } func loginByOpenid(ctx context.Context, openid, sessionKey string) (*v1.WxLoginRes, error) { // 原子 upsert(原生 SQL):小程序启动时 onLaunch 与页面 onShow 会并发各发一次登录, // 冲突时仅更新登录时间。不依赖 ORM 的 OnDuplicate 拼接,行为跨版本稳定。 // session_key 为 NULL 时保留原值,避免空登录把可用于签名的 key 覆盖掉。 now := gtime.Now() var sessionKeyArg interface{} if sessionKey != "" { sessionKeyArg = sessionKey } if _, err := g.DB().Exec(ctx, "INSERT INTO `users`(`openid`, `session_key`, `nickname`, `level_key`, `status`, `created_at`, `last_login_at`, `updated_at`) "+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?) "+ "ON DUPLICATE KEY UPDATE `last_login_at` = VALUES(`last_login_at`), "+ "`session_key` = IF(VALUES(`session_key`) IS NULL, `session_key`, VALUES(`session_key`))", openid, sessionKeyArg, "微信用户", consts.DefaultLevelKey, 1, now, now, now, ); err != nil { return nil, err } record, err := g.Model(consts.TableUsers).Where("openid", openid).One() if err != nil { return nil, err } user := &entity.Users{} if err = record.Struct(user); err != nil { return nil, err } if user.Status != 1 { return nil, gerror.New("账号已被禁用,请联系管理员") } return loginResult(ctx, user) } // ===== H5 账号:注册 / 登录 / 绑定合并 ===== // placeholderOpenid H5 独立账号的 openid 占位(openid 列 NOT NULL UNIQUE,微信 openid 不会以 h5- 开头) func placeholderOpenid() string { return "h5-" + guid.S() } // Register H5 独立账号注册(注册即登录) func Register(ctx context.Context, username, password, nickname string) (*v1.WxLoginRes, error) { count, err := g.Model(consts.TableUsers).Where("username", username).Count() if err != nil { return nil, err } if count > 0 { return nil, gerror.New("用户名已被占用") } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, err } if nickname == "" { nickname = username } now := gtime.Now() if _, err = g.Model(consts.TableUsers).Data(g.Map{ "openid": placeholderOpenid(), "username": username, "password_hash": string(hash), "nickname": nickname, "level_key": consts.DefaultLevelKey, "status": 1, "created_at": now, "last_login_at": now, }).Insert(); err != nil { return nil, err } return loginByUsername(ctx, username, password) } // AccountLogin H5 账号密码登录 func AccountLogin(ctx context.Context, username, password string) (*v1.WxLoginRes, error) { return loginByUsername(ctx, username, password) } func loginByUsername(ctx context.Context, username, password string) (*v1.WxLoginRes, error) { record, err := g.Model(consts.TableUsers).Where("username", username).One() if err != nil { return nil, err } if record.IsEmpty() { return nil, gerror.New("用户名或密码错误") } user := &entity.Users{} if err = record.Struct(user); err != nil { return nil, err } if user.PasswordHash == "" || bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) != nil { return nil, gerror.New("用户名或密码错误") } if user.Status != 1 { return nil, gerror.New("账号已被禁用,请联系管理员") } // 更新登录时间(fire-and-forget 语义,失败不影响登录) _, _ = g.Model(consts.TableUsers).Where("id", user.Id).Data(g.Map{"last_login_at": gtime.Now()}).Update() return loginResult(ctx, user) } // BindAccount 把一个 H5 独立账号合并进当前登录账号: // 迁移其工作台配置 → 将用户名/密码挂到当前账号 → 目标账号禁用保留历史。 // 合并后两端使用同一份用户数据(工作台/等级/反馈)。 func BindAccount(ctx context.Context, username, password string) (*v1.BindAccountRes, error) { current, err := getUserById(ctx, CtxUserId(ctx)) if err != nil { return nil, err } record, err := g.Model(consts.TableUsers).Where("username", username).One() if err != nil { return nil, err } if record.IsEmpty() { return nil, gerror.New("该账号不存在,请先在 H5 端注册") } target := &entity.Users{} if err = record.Struct(target); err != nil { return nil, err } // 已是当前账号:视为重复操作 if target.Id == current.Id { return nil, gerror.New("该账号已是当前账号") } // 只允许合并 H5 独立账号(openid 为 h5- 占位),微信账号不可被合并 if target.Openid == current.Openid || !(len(target.Openid) > 3 && target.Openid[:3] == "h5-") { return nil, gerror.New("该账号已绑定微信,无法合并") } if target.PasswordHash == "" || bcrypt.CompareHashAndPassword([]byte(target.PasswordHash), []byte(password)) != nil { return nil, gerror.New("用户名或密码错误") } if current.Username != "" { return nil, gerror.New("当前账号已绑定过 H5 账号") } // 迁移工作台:目标 keys 追加到当前 keys 之后(去重,上限 20) targetKeys := workbenchKeysOf(ctx, target.Id) if len(targetKeys) > 0 { currentKeys := workbenchKeysOf(ctx, current.Id) seen := map[string]bool{} merged := make([]string, 0, len(currentKeys)+len(targetKeys)) for _, k := range append(append([]string{}, currentKeys...), targetKeys...) { if !seen[k] { seen[k] = true merged = append(merged, k) } } if len(merged) > 20 { merged = merged[:20] } if err = saveWorkbenchRaw(ctx, current.Id, merged); err != nil { return nil, err } } // 先释放目标账号的用户名(username 可空,唯一索引不冲突),并禁用保留历史 if _, err = g.Model(consts.TableUsers).Where("id", target.Id).Data(g.Map{ "username": nil, "status": 0, }).Update(); err != nil { return nil, err } // 凭证挂到当前账号 if _, err = g.Model(consts.TableUsers).Where("id", current.Id).Data(g.Map{ "username": username, "password_hash": target.PasswordHash, }).Update(); err != nil { return nil, err } return &v1.BindAccountRes{Username: username}, nil } // ProfileUpdate 更新当前用户昵称 / 头像 func ProfileUpdate(ctx context.Context, nickname, avatarUrl string) (*v1.ProfileUpdateRes, error) { userId := CtxUserId(ctx) if _, err := g.Model(consts.TableUsers).Where("id", userId).Data(g.Map{ "nickname": nickname, "avatar_url": avatarUrl, }).Update(); err != nil { return nil, err } return &v1.ProfileUpdateRes{Nickname: nickname, AvatarUrl: avatarUrl}, nil } func workbenchKeysOf(ctx context.Context, userId int64) []string { record, err := g.Model(consts.TableWorkbench).Where("user_id", userId).One() if err != nil || record.IsEmpty() { return nil } keys := []string{} _ = json.Unmarshal([]byte(record["tool_keys"].String()), &keys) return keys } // saveWorkbenchRaw 直接写入工作台配置(绑定合并用,不做逐个工具校验) func saveWorkbenchRaw(ctx context.Context, userId int64, keys []string) error { jsonStr, err := json.Marshal(keys) if err != nil { return err } _, err = g.Model(consts.TableWorkbench).Data(g.Map{ "user_id": userId, "tool_keys": string(jsonStr), "updated_at": gtime.Now(), }).Save() return err } // loginResult 统一组装登录返回(token + 用户 + 等级) func loginResult(ctx context.Context, user *entity.Users) (*v1.WxLoginRes, error) { // 等级取「实际生效」值:会员过期后 token 里不能残留高等级 effectiveKey := EffectiveLevelKey(user) level, err := GetLevelByKey(ctx, effectiveKey) if err != nil { return nil, err } token, err := IssueToken(ctx, consts.AudUser, user.Id, map[string]string{"levelKey": effectiveKey}) if err != nil { return nil, err } return &v1.WxLoginRes{ Token: token, User: &v1.UserInfo{Id: user.Id, Nickname: user.Nickname, AvatarUrl: user.AvatarUrl}, Level: level, }, nil } func getUserById(ctx context.Context, id int64) (*entity.Users, error) { record, err := g.Model(consts.TableUsers).Where("id", id).One() if err != nil { return nil, err } if record.IsEmpty() { return nil, gerror.NewCode(gcodeUnauthorized(), "用户不存在") } user := &entity.Users{} if err = record.Struct(user); err != nil { return nil, err } return user, nil } // GetLevelByKey 查询等级(含已授权模块 key 列表) func GetLevelByKey(ctx context.Context, key string) (*v1.LevelInfo, error) { record, err := g.Model(consts.TableLevels).Where("level_key", key).Where("is_enabled", 1).One() if err != nil { return nil, err } if record.IsEmpty() { // 等级被停用或不存在:回落为空权限,避免越权 return &v1.LevelInfo{LevelKey: key, Name: "未授权", Modules: []string{}}, nil } return levelInfoFromRecord(record), nil } func levelInfoFromRecord(record gdb.Record) *v1.LevelInfo { modules := []string{} _ = json.Unmarshal([]byte(record["modules"].String()), &modules) return &v1.LevelInfo{ LevelKey: record["level_key"].String(), Name: record["name"].String(), Modules: modules, } }