完成用户模块后端接口

This commit is contained in:
lulz1 2024-10-25 16:51:57 +08:00
parent a8a7fadcb1
commit 40bf977f5b
10 changed files with 7488 additions and 0 deletions

View File

@ -0,0 +1,52 @@
package user
import (
"context"
"encoding/json"
"time"
)
const UserKey = "user"
type Service interface {
// Register 注册用户,注意这里只是将用户注册, 并没有激活, 需要调用
// 参数user必填usernamepassword, email
// 返回值: user 带上token
Register(ctx context.Context, user *User) (*User, error)
// SendRegisterMail 发送注册的邮件
// 参数user必填 username, password, email, token
SendRegisterMail(ctx context.Context, user *User) error
// VerifyRegister 注册用户,验证注册信息, 返回验证是否成功
VerifyRegister(ctx context.Context, token string) (bool, error)
// Login 登录相关使用用户名密码登录获取完成User信息
Login(ctx context.Context, user *User) (*User, error)
// Logout 登出
Logout(ctx context.Context, user *User) error
// VerifyLogin 登录验证
VerifyLogin(ctx context.Context, token string) (*User, error)
// GetUser 获取用户信息
GetUser(ctx context.Context, userID int64) (*User, error)
}
// User 代表一个用户,注意这里的用户信息字段在不同接口和参数可能为空
type User struct {
ID int64 `gorm:"column:id;primary_key;auto_increment" json:"id"` // 代表用户id, 只有注册成功之后才有这个id唯一表示一个用户
UserName string `gorm:"column:username;type:varchar(255);not null" json:"username"`
Password string `gorm:"column:password;type:varchar(255);not null" json:"password"`
Email string `gorm:"column:email;type:varchar(255);not null" json:"email"`
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;<-:create" json:"createdAt"`
Token string `gorm:"-"` // token 可以用作注册token或者登录token
}
// MarshalBinary 实现BinaryMarshaler 接口
func (b *User) MarshalBinary() ([]byte, error) {
return json.Marshal(b)
}
// UnmarshalBinary 实现 BinaryUnMarshaler 接口
func (b *User) UnmarshalBinary(bt []byte) error {
return json.Unmarshal(bt, b)
}

View File

@ -0,0 +1,31 @@
package user
import (
"github.com/Superdanda/hade/framework"
)
type UserProvider struct {
framework.ServiceProvider
c framework.Container
}
func (sp *UserProvider) Name() string {
return UserKey
}
func (sp *UserProvider) Register(c framework.Container) framework.NewInstance {
return NewUserService
}
func (sp *UserProvider) IsDefer() bool {
return false
}
func (sp *UserProvider) Params(c framework.Container) []interface{} {
return []interface{}{c}
}
func (sp *UserProvider) Boot(c framework.Container) error {
return nil
}

View File

@ -0,0 +1,235 @@
package user
import (
"context"
"crypto/rand"
"fmt"
"github.com/Superdanda/hade/framework"
"github.com/Superdanda/hade/framework/contract"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"gopkg.in/gomail.v2"
"gorm.io/gorm"
"time"
)
type UserService struct {
container framework.Container
logger contract.Log
config contract.Config
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// 生成指定长度的随机令牌
func genToken(n int) (string, error) {
b := make([]byte, n) // 创建存储随机字节的切片
letterLen := byte(len(letterBytes)) // 预存字母表长度,避免重复计算
// 遍历生成随机字节
for i := range b {
randomByte := make([]byte, 1) // 每次生成一个随机字节
if _, err := rand.Read(randomByte); err != nil {
return "", err // 出现错误时返回错误
}
b[i] = letterBytes[randomByte[0]%letterLen] // 将随机字节映射为字母表中的字符
}
return string(b), nil // 返回生成的令牌
}
func (u *UserService) Register(ctx context.Context, user *User) (*User, error) {
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
//验证用户是否已经存在
_, err, _ := u.checkUserNameOrEmailIfExist(ormService, user)
if err != nil {
return nil, err
}
//创建一个令牌
token, err := genToken(10)
if err != nil {
return nil, err
}
user.Token = token
// 将请求注册进入redis保存一天
cacheService := u.container.MustMake(contract.CacheKey).(contract.CacheService)
key := fmt.Sprintf("user:register:%v", user.Token)
if err := cacheService.SetObj(ctx, key, user, 24*time.Hour); err != nil {
return nil, err
}
return user, nil
}
func (u *UserService) SendRegisterMail(ctx context.Context, user *User) error {
logger := u.container.MustMake(contract.LogKey).(contract.Log)
configer := u.container.MustMake(contract.ConfigKey).(contract.Config)
// 配置服务中获取发送邮件需要的参数
host := configer.GetString("app.smtp.host")
port := configer.GetInt("app.smtp.port")
username := configer.GetString("app.smtp.username")
password := configer.GetString("app.smtp.password")
from := configer.GetString("app.smtp.from")
domain := configer.GetString("app.domain")
// 实例化gomail
d := gomail.NewDialer(host, port, username, password)
// 组装message
m := gomail.NewMessage()
m.SetHeader("From", from)
m.SetAddressHeader("To", user.Email, user.UserName)
m.SetHeader("Subject", "感谢您注册我们的网址")
link := fmt.Sprintf("%v/user/register/verify?token=%v", domain, user.Token)
m.SetBody("text/html", fmt.Sprintf("请点击下面的链接完成注册:%s", link))
// 发送电子邮件
if err := d.DialAndSend(m); err != nil {
logger.Error(ctx, "send email error", map[string]interface{}{
"err": err,
"message": m,
})
return err
}
return nil
}
func (u *UserService) VerifyRegister(ctx context.Context, token string) (bool, error) {
container := u.container
cacheService := container.MustMake(contract.CacheKey).(contract.CacheService)
key := fmt.Sprintf("user:register:%v", token)
user := &User{}
if err := cacheService.GetObj(ctx, key, user); err != nil {
return false, err
}
if user.Token != token {
return false, nil
}
//验证邮箱,用户名的唯一
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
//验证用户是否已经存在
_, err, _ := u.checkUserNameOrEmailIfExist(ormService, user)
if err != nil {
return false, err
}
//如果没有重复,将用户数据保存到数据库
// 验证成功将密码存储数据库之前需要加密,不能原文存储进入数据库
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.MinCost)
if err != nil {
return false, err
}
user.Password = string(hash)
// 具体在数据库创建用户
db, err := ormService.GetDB()
if err != nil {
return false, err
}
if err := db.Create(user).Error; err != nil {
return false, err
}
return true, nil
}
func (u *UserService) Login(ctx context.Context, user *User) (*User, error) {
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
db, err := ormService.GetDB()
if err != nil {
return nil, err
}
userDB := &User{}
if err := db.Where("username=?", user.UserName).First(userDB).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.Wrap(err, "该用户未注册")
}
return nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(userDB.Password), []byte(user.Password)); err != nil {
return nil, err
}
userDB.Password = ""
// 缓存session
cacheService := u.container.MustMake(contract.CacheKey).(contract.CacheService)
token, err := genToken(10)
key := fmt.Sprintf("session:%v", token)
if err := cacheService.SetObj(ctx, key, userDB, 24*time.Hour); err != nil {
return nil, err
}
userDB.Token = token
return userDB, nil
}
func (u *UserService) Logout(ctx context.Context, user *User) error {
cacheService := u.container.MustMake(contract.CacheKey).(contract.CacheService)
userSession, err := u.VerifyLogin(ctx, user.Token)
// 不需要做任何操作
if err != nil || userSession.UserName != user.UserName {
return nil
}
key := fmt.Sprintf("session:%v", user.Token)
_ = cacheService.Del(ctx, key)
return nil
}
func (u *UserService) VerifyLogin(ctx context.Context, token string) (*User, error) {
if token == "" {
return nil, errors.New("token不能为空")
}
cacheService := u.container.MustMake(contract.CacheKey).(contract.CacheService)
key := fmt.Sprintf("session:%v", token)
user := &User{}
if err := cacheService.GetObj(ctx, key, user); err != nil {
return nil, err
}
return user, nil
}
func (u *UserService) GetUser(ctx context.Context, userID int64) (*User, error) {
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
db, err := ormService.GetDB()
if err != nil {
return nil, err
}
user := &User{ID: userID}
if err := db.WithContext(ctx).First(user).Error; err != nil {
return nil, err
}
return user, nil
}
func NewUserService(params ...interface{}) (interface{}, error) {
container := params[0].(framework.Container)
logger := container.MustMake(contract.LogKey).(contract.Log)
config := container.MustMake(contract.ConfigKey).(contract.Config)
return &UserService{container: container, logger: logger, config: config}, nil
}
func (s *UserService) Foo() string {
return ""
}
func (s *UserService) checkUserNameOrEmailIfExist(dbService contract.ORMService, user *User) (bool, error, *User) {
db, err := dbService.GetDB()
userDB := &User{}
if err != nil {
return false, err, nil
}
if !errors.Is(db.Where(&User{Email: user.Email}).First(userDB).Error, gorm.ErrRecordNotFound) {
return true, errors.New("邮箱已注册用户,不能重复注册"), userDB
}
if !errors.Is(db.Where(&User{UserName: user.UserName}).First(userDB).Error, gorm.ErrRecordNotFound) {
return true, errors.New("邮箱已注册用户,不能重复注册"), userDB
}
return false, nil, nil
}

6917
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

71
src/router/index.js Normal file
View File

@ -0,0 +1,71 @@
import Vue from 'vue'
import Router from 'vue-router'
import ViewLogin from '../views/login/index'
import ViewRegister from '../views/register/index'
import View404 from '../views/404'
Vue.use(Router)
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes = [
{
path: '/login',
component: ViewLogin,
hidden: true
},
{
path: '/register',
component: ViewRegister,
hidden: true
},
{
path: '/404',
component: View404,
hidden: true
},
// {
// path: '/',
// redirect: '/',
// component: ViewContainer,
// children: [
// {
// path: '',
// component: ViewList
// },
// {
// path: 'detail',
// component: ViewDetail
// },
// {
// path: 'create',
// component: ViewCreate
// },
// {
// path: 'edit',
// component: ViewEdit
// }
// ]
// },
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router

11
src/store/getters.js Normal file
View File

@ -0,0 +1,11 @@
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
size: state => state.app.size,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
}
export default getters

25
src/store/index.js Normal file
View File

@ -0,0 +1,25 @@
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
Vue.use(Vuex)
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', true, /\.js$/)
// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
// set './app.js' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
const store = new Vuex.Store({
modules,
getters
})
export default store

75
src/store/modules/user.js Normal file
View File

@ -0,0 +1,75 @@
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
import request from "../../utils/request";
const getDefaultState = () => {
return {
token: getToken(),
name: 'jianfengye',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'
}
}
const state = getDefaultState()
const mutations = {
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
}
}
const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
request.post("/user/login",{ username: username.trim(), password: password }).then(response => {
const token = response.data
commit('SET_TOKEN', token)
setToken(token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// user logout
logout({ commit, state }) {
return new Promise((resolve, reject) => {
request("/user/logout", state.token).then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
removeToken() // must remove token first
commit('RESET_STATE')
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

15
src/utils/auth.js Normal file
View File

@ -0,0 +1,15 @@
import Cookies from 'js-cookie'
const TokenKey = 'hade_bbs'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}

56
src/utils/request.js Normal file
View File

@ -0,0 +1,56 @@
import axios from 'axios'
import { Message } from 'element-ui'
// 创建一个axios
const service = axios.create({
withCredentials: true, // send cookies when cross-domain requests
timeout: 10000 // request timeout
})
// 请求的配置
service.interceptors.request.use(
config => {
return config
},
error => {
// 如果request 有错误,打印信息
console.log(error) // for debug
return Promise.reject(error)
}
)
// response中统一做处理
service.interceptors.response.use(
response => {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
// 判断http status是否为200
if (response.status !== 200) {
const data = response.data
if (typeof data == 'string') {
Message({
message: data,
type: 'error',
duration: 5 * 1000
})
}
}
return response
},
error => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
console.log('err: ' + error) // for debug
// 打印Message消息
Message({
message: error.response.data,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service