查询用户列表

dev
truthhun 2 years ago
parent ee4d7a7215
commit c070492eb0

@ -58,7 +58,11 @@ message GetUserRequest { int64 id = 1; }
message ListUserRequest {
int64 page = 1;
int64 size = 2;
repeated int64 ids = 3;
string wd = 3;
string sort = 4;
repeated int64 id = 5;
repeated int64 group_id = 6;
repeated int32 status = 7;
}
message ListUserReply {

@ -2,6 +2,7 @@ package biz
import (
"context"
"strings"
"time"
pb "moredoc/api/v1"
@ -15,9 +16,9 @@ import (
"github.com/alexandrevicenzi/unchained"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"gorm.io/gorm"
)
const (
@ -75,8 +76,8 @@ func (s *UserAPIService) Register(ctx context.Context, req *pb.RegisterAndLoginR
}
user := &model.User{Username: req.Username, Password: req.Password}
if p, ok := peer.FromContext(ctx); ok {
user.RegisterIp = p.Addr.String()
if ips, _ := util.GetGRPCRemoteIP(ctx); len(ips) > 0 {
user.RegisterIp = ips[0]
}
group, _ := s.dbModel.GetDefaultUserGroup()
@ -123,8 +124,8 @@ func (s *UserAPIService) Login(ctx context.Context, req *pb.RegisterAndLoginRequ
util.CopyStruct(&user, pbUser)
ip := ""
if p, ok := peer.FromContext(ctx); ok {
ip = p.Addr.String()
if ips, _ := util.GetGRPCRemoteIP(ctx); len(ips) > 0 {
ip = ips[0]
}
if e := s.dbModel.UpdateUser(&model.User{Id: user.Id, LoginAt: time.Now(), LastLoginIp: ip}, "login_at", "last_login_ip"); e != nil {
s.logger.Error("UpdateUser", zap.Error(e))
@ -239,8 +240,68 @@ func (s *UserAPIService) DeleteUser(ctx context.Context, req *pb.DeleteUserReque
return &emptypb.Empty{}, nil
}
// ListUser 查询用户列表
// 1. 非管理员,只能查询公开信息
// 2. 管理员,可以查询全部信息
func (s *UserAPIService) ListUser(ctx context.Context, req *pb.ListUserRequest) (*pb.ListUserReply, error) {
return &pb.ListUserReply{}, nil
var (
userId int64
limitFileds = model.UserPublicFields
fullMethod, _ = ctx.Value(auth.CtxKeyFullMethod).(string)
)
userClaims, ok := ctx.Value(auth.CtxKeyUserClaims).(*auth.UserClaims)
if ok && !s.dbModel.IsInvalidToken(userClaims.UUID) {
userId = userClaims.UserId
}
opt := model.OptionGetUserList{
Page: int(req.Page),
Size: int(req.Size_),
WithCount: true,
}
if len(req.Id) > 0 {
var ids []interface{}
for _, id := range req.Id {
ids = append(ids, id)
}
opt.WithCount = false
opt.Ids = ids
}
if len(req.GroupId) > 0 {
var groupIds []interface{}
for _, groupId := range req.GroupId {
groupIds = append(groupIds, groupId)
}
opt.QueryIn = map[string][]interface{}{"group_id": groupIds}
}
if req.Sort != "" {
opt.Sort = strings.Split(req.Sort, ",")
}
if s.dbModel.CheckPermissionByUserId(userId, "", fullMethod) {
limitFileds = []string{} // 管理员,可以查询全部信息
if req.Wd != "" {
opt.QueryLike = map[string][]interface{}{
"username": {"%" + strings.TrimSpace(req.Wd) + "%"},
}
}
}
opt.SelectFields = limitFileds
var pbUser []*pb.User
userList, total, err := s.dbModel.GetUserList(opt)
if err == gorm.ErrRecordNotFound {
err = nil
return &pb.ListUserReply{}, nil
}
util.CopyStruct(&userList, &pbUser)
s.logger.Debug("ListUser", zap.Any("userList", userList), zap.Any("pbUser", pbUser), zap.Int64("total", total))
return &pb.ListUserReply{Total: total, User: pbUser}, nil
}
// GetUserCaptcha 获取用户验证码

@ -8,9 +8,9 @@
- [x] 用户注册
- [x] 用户登录
- [ ] 修改用户资料:普通用户修改个人资料,管理员可修改其他用户资料
- [ ] 查询个人资料
- [ ] 修改个人密码
- [x] 修改用户资料:普通用户修改个人资料,管理员可修改其他用户资料
- [x] 查询个人资料
- [x] 修改个人密码
- [ ] 退出登录
- [ ] 删除用户
- [ ] 查看用户列表

@ -86,7 +86,7 @@ func (m *DBModel) GetPermissionByMethodPath(method, path string, createIfNotExis
if method != "" {
db = db.Where("method = ?", method)
} else {
db = db.Where("(method IS NULL or method = '')")
db = db.Where("method IS NULL or method = ''")
}
err = db.First(&permission).Error
@ -128,9 +128,10 @@ func (m *DBModel) CheckPermissionByUserId(userId int64, method, path string) (ye
)
// NOTE: ID为1的用户拥有所有权限可以理解为类似linux的root用户
if userId == 1 {
return true
}
// TODO: 后续应该直接打开这里的注释
// if userId == 1 {
// return true
// }
if userId > 0 {
m.db.Where("user_id = ?", userId).Find(&userGroups)
@ -139,14 +140,15 @@ func (m *DBModel) CheckPermissionByUserId(userId int64, method, path string) (ye
}
}
return m.CheckPermissionByGroupId(groupId, method, path)
yes = m.CheckPermissionByGroupId(groupId, method, path)
return yes || userId == 1
}
// CheckPermissionByGroupId 根据用户所属用户组ID检查用户是否有权限
func (m *DBModel) CheckPermissionByGroupId(groupId []int64, method, path string) (yes bool) {
fields := []string{"id", "type", "method", "path"}
fields := []string{"id", "method", "path"}
permission, err := m.GetPermissionByMethodPath(method, path, true, fields...)
if err != nil {
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("CheckPermissionByGroupId", zap.Error(err))
}

@ -18,6 +18,13 @@ const (
UserStatusIgnored // 忽略
)
// 用户的公开信息字段
var UserPublicFields = []string{
"id", "username", "signature", "status", "avatar", "realname",
"doc_count", "follow_count", "fans_count", "favorite_count", "comment_count",
"created_at", "updated_at", "login_at",
}
type User struct {
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:用户 id;"`
Username string `form:"username" json:"username,omitempty" gorm:"column:username;type:varchar(64);size:64;index:username,unique;comment:用户名;"`
@ -234,8 +241,8 @@ func (m *DBModel) GetUserList(opt OptionGetUserList) (userList []User, total int
db = db.Select(opt.SelectFields)
}
var sorts []string
if len(opt.Sort) > 0 {
var sorts []string
for _, sort := range opt.Sort {
slice := strings.Split(sort, " ")
if len(m.FilterValidFields(User{}.TableName(), slice[0])) == 0 {
@ -248,11 +255,13 @@ func (m *DBModel) GetUserList(opt OptionGetUserList) (userList []User, total int
sorts = append(sorts, fmt.Sprintf("%s desc", slice[0]))
}
}
if len(sorts) > 0 {
db = db.Order(strings.Join(sorts, ","))
}
} else {
sorts = append(sorts, "id desc")
}
if len(sorts) > 0 {
db = db.Order(strings.Join(sorts, ","))
}
db = db.Offset((opt.Page - 1) * opt.Size).Limit(opt.Size)
err = db.Find(&userList).Error

@ -1,9 +1,41 @@
package util
import jsoniter "github.com/json-iterator/go"
import (
"context"
"strings"
jsoniter "github.com/json-iterator/go"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
)
// CopyStruct 拷贝。注意只能拷贝相同类型的结构体且结构体中有json标签
func CopyStruct(srcPtr, dstPtr interface{}) {
bytes, _ := jsoniter.Marshal(srcPtr)
jsoniter.Unmarshal(bytes, dstPtr)
}
// GetGRPCRemoteIP 获取用户IP
func GetGRPCRemoteIP(ctx context.Context) (ips []string, err error) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
headers := []string{"x-real-ip", "x-forwarded-for", "remote-addr"}
for _, header := range headers {
if values := md.Get(header); len(values) > 0 {
for _, item := range values {
ips = append(ips, strings.Split(item, ",")...)
}
}
}
}
// 如果是 grpc client 直接调用该接口则用这种方式来获取真实的IP地址
if p, ok := peer.FromContext(ctx); ok {
arr := strings.Split(p.Addr.String(), ":")
if l := len(arr); l > 0 {
ip := strings.Join(arr[0:l-1], ":")
ip = strings.NewReplacer("[", "", "]", "").Replace(ip)
ips = append(ips, ip)
}
}
return
}

@ -51,6 +51,22 @@ body{
color: #409eff;
}
}
.el-table th{
height: 45px;
line-height: 45px;
padding: 5px 0;
&.el-table__cell {
background-color: #f7fbff;
color: #000;
font-weight: normal;
&.el-table-column--selection>.cell{
padding-left: 14px;
}
}
&.el-table__cell.is-leaf{
border-bottom: 0;
}
}
}
.page-login{
@ -60,7 +76,3 @@ body{
margin: 20vh auto;
}
}
.page-admin-projects{
}

@ -0,0 +1,130 @@
<template>
<div class="com-table-list">
<el-table
:data="tableData"
style="width: 100%"
@selection-change="selectRow"
>
<el-table-column v-if="showSelect" type="selection" width="55">
</el-table-column>
<el-table-column
v-for="item in fields"
:key="'field-' + item.prop"
:prop="item.prop"
:label="item.label"
:width="item.width"
:min-width="item.minWidth"
:fixed="item.fixed"
>
<template slot-scope="scope">
<!-- 头像 -->
<el-avatar
v-if="item.type === 'avatar'"
:size="45"
:src="scope.row[item.prop]"
>
<img src="/static/images/blank.png" />
</el-avatar>
<!-- 数字 -->
<span v-else-if="item.type === 'number'">{{
scope.row[item.prop] || '0'
}}</span>
<span v-else-if="item.type === 'datetime'">
{{ formatDatetime(scope.row[item.prop]) || '0000-00-00 00:00:00' }}
</span>
<!-- 字符串更多则需要继续扩展 -->
<span v-else>{{ scope.row[item.prop] || '-' }}</span>
</template>
</el-table-column>
<el-table-column
v-if="showActions || showView || showDelete || showEdit"
fixed="right"
label="操作"
:min-width="actionsMinWidth"
>
<template slot-scope="scope">
<el-button
v-if="showView"
type="text"
size="small"
icon="el-icon-view"
@click="viewRow(scope.row)"
>查看</el-button
>
<el-button
v-if="showEdit"
type="text"
size="small"
icon="el-icon-edit"
@click="editRow(row)"
>编辑</el-button
>
<el-button
v-if="showDelete"
type="text"
size="small"
icon="el-icon-delete"
@click="deleteRow(scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { formatDatetime } from '~/utils/utils'
export default {
name: 'ComTableList',
props: {
tableData: {
type: Array,
default: () => [],
},
// fieldlabelpropwidthmin-widthfixedtypetypetextimagelinkbuttonslot
fields: {
type: Array,
default: () => [],
},
actionsMinWidth: {
type: Number,
default: 180,
},
showActions: {
type: Boolean,
default: true,
},
showView: {
type: Boolean,
default: true,
},
showEdit: {
type: Boolean,
default: true,
},
showDelete: {
type: Boolean,
default: true,
},
showSelect: {
type: Boolean,
default: true,
},
},
methods: {
formatDatetime,
viewRow(row) {
this.$emit('viewRow', row)
},
editRow(row) {
this.$emit('editRow', row)
},
deleteRow(row) {
this.$emit('deleteRoW', row)
},
selectRow(val) {
this.$emit('selectRow', val)
},
},
}
</script>

@ -131,13 +131,13 @@
<el-main>
<nuxt />
</el-main>
<el-footer>
<!-- <el-footer>
<span>© 2019-2022</span>
<span>Powered by</span>
<a href="https://mnt.ltd" target="_blank" title="MOREDOC"
>MOREDOC · 魔刀文库</a
>
</el-footer>
</el-footer> -->
</el-container>
<el-dialog title="个人资料" :visible.sync="formProfileVisible" width="30%">
<FormProfile @success="profileSuccess" />

@ -1,10 +1,127 @@
<template>
<div>{{ $route.name }}</div>
<div>
<!-- <el-table :data="users" style="width: 100%">
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="id" label="ID" width="80"> </el-table-column>
<el-table-column prop="avatar" label="头像" width="75">
<template slot-scope="scope">
<el-avatar :size="45" :src="scope.row.avatar">
<img src="/static/images/blank.png" />
</el-avatar>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" width="120">
</el-table-column>
<el-table-column prop="realname" label="姓名" width="120">
<template slot-scope="scope">
{{ scope.row.realname || '-' }}
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱" width="120">
<template slot-scope="scope">
{{ scope.row.email || '-' }}
</template>
</el-table-column>
<el-table-column prop="mobile" label="电话" width="120">
<template slot-scope="scope">
{{ scope.row.mobile || '-' }}
</template>
</el-table-column>
<el-table-column prop="address" label="地址" width="150">
<template slot-scope="scope">
{{ scope.row.address || '-' }}
</template>
</el-table-column>
<el-table-column prop="signature" label="签名" width="150">
<template slot-scope="scope">
{{ scope.row.signature || '-' }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" min-width="100">
<template slot-scope="scope">
<el-button
type="text"
size="small"
icon="el-icon-view"
@click="handleClick(scope.row)"
>查看</el-button
>
<el-button type="text" size="small" icon="el-icon-edit"
>编辑</el-button
>
<el-button type="text" size="small" icon="el-icon-delete"
>删除</el-button
>
</template>
</el-table-column>
</el-table> -->
<TableList
:table-data="users"
:fields="fields"
:show-actions="true"
:show-view="true"
:show-edit="true"
:show-delete="true"
:show-select="true"
/>
</div>
</template>
<script>
import { listUser } from '~/api/user'
import TableList from '~/components/TableList.vue'
export default {
components: { TableList },
layout: 'admin',
created() {},
data() {
return {
users: [],
total: 0,
fields: [
{ prop: 'id', label: 'ID', width: 80, type: 'number', fixed: 'left' },
{
prop: 'avatar',
label: '头像',
width: 80,
type: 'avatar',
fixed: 'left',
},
{ prop: 'username', label: '用户名', width: 150, fixed: 'left' },
{ prop: 'doc_count', label: '文档', width: 80, type: 'number' },
{ prop: 'follow_count', label: '关注', width: 80, type: 'number' },
{ prop: 'fans_count', label: '粉丝', width: 80, type: 'number' },
{ prop: 'favorite_count', label: '收藏', width: 80, type: 'number' },
{ prop: 'comment_count', label: '评论', width: 80, type: 'number' },
{ prop: 'realname', label: '姓名', width: 150 },
{ prop: 'email', label: '邮箱', width: 200 },
{ prop: 'mobile', label: '电话', width: 200 },
{ prop: 'identity', label: '身份证', width: 250 },
{ prop: 'address', label: '地址', width: 250 },
{ prop: 'signature', label: '签名', width: 250 },
{ prop: 'created_at', label: '注册时间', width: 160, type: 'datetime' },
{ prop: 'register_ip', label: '注册IP', width: 160 },
{ prop: 'login_at', label: '最后登录', width: 160, type: 'datetime' },
{
prop: 'last_login_ip',
label: '最后登录IP',
width: 160,
},
],
}
},
created() {
this.listUser()
},
methods: {
async listUser() {
const res = await listUser()
if (res.status === 200) {
this.users = res.data.user
this.total = res.data.total
}
console.log(res)
},
},
}
</script>
<style></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Loading…
Cancel
Save