package wechat import ( "context" "fmt" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) // ErrFailedToOperateWechatOfficialAccountUserDB 错误类型:打开数据库失败 type ErrFailedToOperateWechatOfficialAccountUserDB struct { Action string Message string } func (err ErrFailedToOperateWechatOfficialAccountUserDB) Error() string { return fmt.Sprintf("Failed to %v in WeChat Official Account DB: %v", err.Action, err.Message) } // ErrWechatOfficialAccountUserNotExist 错误类型:找不到对应的微信公众号用户 type ErrWechatOfficialAccountUserNotExist struct { AppID string OpenID string } func (err ErrWechatOfficialAccountUserNotExist) Error() string { return fmt.Sprintf("WeChat Official Account User not found: AppID = %v, OpenID = %v", err.AppID, err.OpenID) } // UserWechatOpenid 用户ID与微信公众号openid一对一关联表 // 数据库指定schema下 必须存在 `user_wechat_openid`,否则报错 ERROR 1146 (42S02): Table does not exist // // 遵循gonic规则映射数据库表 `user_wechat_openid`,各字段注解见 https://xorm.io/docs/chapter-02/4.columns/ type UserWechatOpenid struct { Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键')"` Uid int64 `xorm:"BIGINT UNIQUE NOT NULL 'uid' comment('user表主键')"` WechatAppid string `xorm:"VARCHAR(50) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'official_account_appid' comment('微信公众号关联的AppID,便于区分不同公众号用户、便于后期公众号迁移')"` Openid string `xorm:"VARCHAR(50) charset=utf8mb4 collate=utf8mb4_bin UNIQUE NOT NULL 'openid' comment('微信公众号粉丝OpenID')"` } // Use `charset=utf8mb4 collate=utf8mb4_bin` to fix log.Error: There are 2 table columns(`official_account_appid`, `openid`) using inconsistent collation, they should use "utf8mb4_bin". Please go to admin panel Self Check page func init() { db.RegisterModel(new(UserWechatOpenid)) } // QueryUserByOpenid 根据微信公众号用户openid查询 `user` 表用户 func QueryUserByOpenid(ctx context.Context, openid string) (*user_model.User, error) { // 1. 根据公众号 AppID 和 OpenID 查询数据库中 user表 主键ID wechatUser := &UserWechatOpenid{ WechatAppid: setting.Wechat.UserConfig.AppID, Openid: openid, } var user *user_model.User // 2. 开启数据库事务,查询用户信息 err := db.WithTx(ctx, func(ctx context.Context) error { // 2.1 根据 `user_wechat_openid` 表 查询用户 openid exist, err := db.GetEngine(ctx).Get(wechatUser) // 注: 断点必须打在操作数据库之后,否则超时导致报错: context canceled if err != nil { return ErrFailedToOperateWechatOfficialAccountUserDB{ Action: fmt.Sprintf("Find WeChat openid '%s'", openid), Message: err.Error(), } } if !exist { return ErrWechatOfficialAccountUserNotExist{ AppID: wechatUser.WechatAppid, OpenID: wechatUser.Openid, } } // 2.2 根据 `user` 表 主键ID查询用户信息 user, err = user_model.GetUserByID(ctx, wechatUser.Uid) if err != nil { return err } // 数据库事务完成,返回nil自动进行Commit return nil }) if err != nil { return nil, err } // 3. 检查用户是否被禁用,参考校验逻辑 services/auth/signin.go#UserSignIn. if user.ProhibitLogin { return nil, user_model.ErrUserProhibitLogin{UID: user.ID, Name: user.Name} } return user, err } // UpdateOrCreateWechatUser 更新/新建用户微信 func UpdateOrCreateWechatUser(ctx context.Context, user *user_model.User, openid string) error { // 1. 构造查询条件:DevStar 用户ID,即 UID wechatUser := &UserWechatOpenid{ Uid: user.ID, } WechatAppid := setting.Wechat.UserConfig.AppID // 2. 开启数据库事务,保证数据库表 `user_wechat_openid` 原子操作(出错时候自动进行 transaction Rollback) err := db.WithTx(ctx, func(ctx context.Context) error { // 2.1 根据 UID 查询数据库 has, err := db.GetEngine(ctx).Get(wechatUser) if err != nil { return ErrFailedToOperateWechatOfficialAccountUserDB{ Action: fmt.Sprintf("Find UserID %v", user.ID), Message: err.Error(), } } // 2.2 数据库更新或插入新记录 if has && wechatUser.WechatAppid == WechatAppid && wechatUser.Openid == openid { // 有用户记录,且未发生变化,直接返回,结束事务 return nil } else if has && (wechatUser.WechatAppid != WechatAppid || wechatUser.Openid != openid) { // 有用户记录,但公众号主体 AppId 或者 关联的用户号 OpenID 发生变化 wechatUser.WechatAppid = WechatAppid wechatUser.Openid = openid _, err = db.GetEngine(ctx).ID(wechatUser.Id).Update(wechatUser) if err != nil { return ErrFailedToOperateWechatOfficialAccountUserDB{ Action: fmt.Sprintf("Update existing UID %v", user.ID), Message: err.Error(), } } } else { // 没有用户记录,需要插入新纪录 wechatUser.WechatAppid = WechatAppid wechatUser.Openid = openid _, err = db.GetEngine(ctx).Insert(wechatUser) if err != nil { return ErrFailedToOperateWechatOfficialAccountUserDB{ Action: fmt.Sprintf("Insert new user UID = %v", user.ID), Message: err.Error(), } } } return nil }) if err != nil { return ErrFailedToOperateWechatOfficialAccountUserDB{ Action: "Start an DB transaction", Message: err.Error(), } } log.Info("微信公众号绑定成功: (UID, User.Name, OpenID) = (%v, %v, %v)", user.ID, user.Name, openid) return nil } // DeleteWechatUser 清空当前用户的微信公众号绑定信息 func DeleteWechatUser(ctx context.Context, user *user_model.User) error { // 0. 检查参数 if user == nil || user.ID <= 0 { return ErrFailedToOperateWechatOfficialAccountUserDB{ Action: "Find User", Message: "User and its ID cannot be nil", } } // 1. 构造查询条件:DevStar 用户ID,即 UID wechatUser := &UserWechatOpenid{ Uid: user.ID, } // 2. 开启数据库事务,删除数据库表 `user_wechat_openid` 中 user.ID 对应条目 err := db.WithTx(ctx, func(ctx context.Context) error { // 2.1 根据 UID 查询数据库 _, err := db.GetEngine(ctx).Delete(wechatUser) if err != nil { return ErrFailedToOperateWechatOfficialAccountUserDB{ Action: fmt.Sprintf("Delete WeChat Official Account Binding for User '%v'(ID = %v)", user.Name, user.ID), Message: err.Error(), } } return nil }) return err }