[2024-08-09](UPDATE): 分页查询

This commit is contained in:
june 2024-08-09 20:30:35 +08:00
parent 84a5c80057
commit 5feaf4ef49
12 changed files with 315 additions and 55 deletions

View File

@ -7,13 +7,14 @@ type PaginationData struct {
NumPages int `json:"num_pages"` // 总页数
LeftHasMore bool `json:"left_has_more"` // 左边显示更多
RightHasMore bool `json:"right_has_more"` // 右边显示更多
Total int64 `json:"total"` // 数据总量
}
// GetPaginationData 分页数据
// numPages 总页数
// currentPage 当前页
// aroundCount 当前页左右两边显示多少个分页按钮
func GetPaginationData(numPages int, currentPage int, aroundCount int) PaginationData {
func GetPaginationData(numPages int, currentPage int, aroundCount int, total int64) PaginationData {
// 边缘条件判断
if currentPage > numPages {
currentPage = numPages
@ -49,7 +50,7 @@ func GetPaginationData(numPages int, currentPage int, aroundCount int) Paginatio
}
return PaginationData{
LeftPages: leftPages, RightPages: rightPages, CurrentPage: currentPage,
NumPages: numPages, LeftHasMore: leftHasMore, RightHasMore: rightHasMore}
NumPages: numPages, LeftHasMore: leftHasMore, RightHasMore: rightHasMore, Total: total}
}
// LeftPages RightPages

View File

@ -8,9 +8,9 @@ import (
)
var (
RdbClient *redis.Client
CaptchaKey = "picgo:captcha:" // 验证码存储key
UserKey = "picgo:user:"
RdbClient *redis.Client
CaptchaKey = "picgo:captcha:" // 验证码存储key
UserNameKey = "picgo:user:username:"
)
func NewRedis() {

View File

@ -26,15 +26,15 @@ func SysUserSelectByUsername(userName string) (model.SysUser, error) {
corelib.Logger.Infoln("SysUserSelectByUsername 从缓存获取用户信息成功: ", user.Username)
return user, nil
} else {
corelib.Logger.Error("SysUserSelectByUsername 从缓存获取用户信息失败: ", userName, " error: ", err)
err = nil
corelib.Logger.Infoln("SysUserSelectByUsername 从缓存获取用户信息失败: ", user.Username)
}
if err = corelib.DbMysql.Model(model.SysUser{Username: userName}).First(&user).Error; err != nil {
corelib.Logger.Infoln("SysUserSelectByUsername 数据库中查询用户信息失败: ", err)
if err = corelib.DbMysql.Where("username = ?", userName).First(&user).Error; err != nil {
corelib.Logger.Error("SysUserSelectByUsername 数据库中查询用户信息失败: ", userName, " error: ", err)
return user, err
}
if err = SysUserSetCacheByUsername(user); err != nil {
corelib.Logger.Infoln("SysUserSelectByUsername 缓存用户信息失败: ", err)
corelib.Logger.Error("SysUserSelectByUsername 缓存用户信息失败: ", err)
return user, nil
}
return user, nil
@ -48,7 +48,7 @@ func SysUserSetCacheByUsername(user model.SysUser) error {
if jsonData, err = corelib.JsonMarshal(user); err != nil {
return err
}
key := corelib.UserKey + user.Username
key := corelib.UserNameKey + user.Username
if err = corelib.RdbClient.Set(context.Background(), key, string(jsonData), 5*time.Minute).Err(); err != nil {
return err
}
@ -62,7 +62,7 @@ func SysUserGetCacheByUsername(userName string) (model.SysUser, error) {
user model.SysUser
userCache string
)
if userCache, err = corelib.RdbClient.Get(context.Background(), corelib.UserKey+userName).Result(); err != nil {
if userCache, err = corelib.RdbClient.Get(context.Background(), corelib.UserNameKey+userName).Result(); err != nil {
return model.SysUser{}, err
}
if err = corelib.JsonUnmarshal([]byte(userCache), &user); err != nil {

View File

@ -31,7 +31,6 @@ func LoginApiHandler(w http.ResponseWriter, r *http.Request) {
res model.LoginRequest
user model.SysUser
)
if err = json.NewDecoder(r.Body).Decode(&res); err != nil {
corelib.Logger.Error("LoginApiHandler, 参数获取失败")
corelib.WriteJsonResponse(w, 400, "参数错误", nil)
@ -44,23 +43,29 @@ func LoginApiHandler(w http.ResponseWriter, r *http.Request) {
}
cid := getCaptchaId(r)
if ok := captcha.Verify(cid, res.Captcha); !ok {
corelib.WriteJsonResponse(w, 1040, "验证码错误", nil)
corelib.Logger.Error("LoginApiHandler, 验证码错误")
corelib.WriteJsonResponse(w, 400, "验证码错误", nil)
return
}
if user, err = data.SysUserSelectByUsername(res.Username); err != nil {
corelib.WriteJsonResponse(w, 1041, "用户不存在", nil)
corelib.Logger.Error("LoginApiHandler, 用户不存在")
corelib.WriteJsonResponse(w, 1040, "用户不存在", nil)
return
}
// 验证用户名密码
if !corelib.ComparePasswords(user.Password, res.Password, user.Salt) {
corelib.WriteJsonResponse(w, 1042, "用户名或密码错误", nil)
corelib.Logger.Error("LoginApiHandler, 用户名或密码错误")
corelib.WriteJsonResponse(w, 1041, "用户名或密码错误", nil)
return
}
session, _ := corelib.SessionStore.Get(r, configs.Settings.Server.SessionName)
session.Values["username"] = user.Username
session.Values["id"] = user.ID
if err = session.Save(r, w); err != nil {
corelib.Logger.Infoln("session save err:", err)
corelib.WriteJsonResponse(w, 1043, "会话保存失败", nil)
corelib.Logger.Error("LoginApiHandler, 会话保存失败:", err)
corelib.WriteJsonResponse(w, 1042, "会话保存失败", nil)
return
}
w.Header().Set("Content-Type", "application/json")

View File

@ -2,6 +2,8 @@ package handler
import (
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/csrf"
"math"
"net/http"
@ -16,18 +18,25 @@ func UserPageHandler(w http.ResponseWriter, r *http.Request) {
var (
err error
user model.SysUser
res model.UserpageResponse
)
username := r.Context().Value("username").(string)
if user, err = data.SysUserGetCacheByUsername(username); err != nil {
http.Error(w, "IndexHandler SysUserGetCacheByUsername Error", http.StatusInternalServerError)
return
}
tmpData := map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
}
tmpData["Title"] = "用户管理"
tmpData["Active"] = r.URL.Path
tmpData["IsSuper"] = user.IsSuper
if res, err = pagination(1, 10, ""); err != nil {
http.Error(w, fmt.Sprintf("%v", err), http.StatusInternalServerError)
return
}
tmpData["Data"] = res
w.Header().Add("X-CSRF-Token", csrf.Token(r))
corelib.TemplateHandler(w, tmpData, "layout.html", "view/layout.html", "view/user.html")
} else {
@ -45,21 +54,18 @@ func UserCreateApiHandler(w http.ResponseWriter, r *http.Request) {
password string
salt string
)
if err = json.NewDecoder(r.Body).Decode(&res); err != nil {
corelib.Logger.Error("UserCreateApiHandler, 参数获取失败")
corelib.WriteJsonResponse(w, 400, "参数错误", nil)
return
}
if (res.Username == "") || (res.Password == "") {
corelib.Logger.Error("UserCreateApiHandler, 用户名或者密码为空")
corelib.WriteJsonResponse(w, 400, "请输入用户名密码", nil)
return
}
if isExists := data.SysUserExists(res.Username); !isExists {
corelib.Logger.Error("UserCreateApiHandler, 用户已经存在")
if isExists := data.SysUserExists(res.Username); isExists {
corelib.Logger.Error("UserCreateApiHandler, 用户: " + res.Username + "已经存在")
corelib.WriteJsonResponse(w, 10050, "用户已经存在", nil)
return
}
@ -92,16 +98,17 @@ func UserCreateApiHandler(w http.ResponseWriter, r *http.Request) {
func UserPageApiHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
var (
err error
page int
onePageCount int
count int
err error
page int
pageSize int
total int64
user []model.SysUser
)
query := r.URL.Query()
// 搜索条件
search := query.Get("search")
// 每页显示多少跳
if onePageCount, err = strconv.Atoi(query.Get("count")); err != nil {
if pageSize, err = strconv.Atoi(query.Get("pageSize")); err != nil {
corelib.Logger.Error("UserPageApiHandler, 获取count参数失败")
corelib.WriteJsonResponse(w, 400, "参数错误", nil)
return
@ -112,15 +119,49 @@ func UserPageApiHandler(w http.ResponseWriter, r *http.Request) {
corelib.WriteJsonResponse(w, 400, "参数错误", nil)
return
}
numPage := math.Ceil(float64(count) / float64(onePageCount))
paginationData := corelib.GetPaginationData(int(numPage), page, 2)
if page < 1 {
page = 1
}
offset := (page - 1) * pageSize
if search != "" {
} else {
if err = corelib.DbMysql.Where("name like ?", "%"+search+"%").Find(&model.SysUser{}).Count(&total).Error; err != nil {
corelib.Logger.Error("UserPageApiHandler, search, 查询total失败")
corelib.WriteJsonResponse(w, 500, "数据查询失败", nil)
}
if err = corelib.DbMysql.Limit(pageSize).Offset(offset).Where("name like ?", "%"+search+"%").Find(&user).Error; err != nil {
corelib.Logger.Error("UserPageApiHandler, search, 查询分页数据失败")
corelib.WriteJsonResponse(w, 500, "数据查询失败", nil)
}
}
if err = corelib.DbMysql.Find(&model.SysUser{}).Count(&total).Error; err != nil {
corelib.Logger.Error("UserPageApiHandler, all, 查询total失败")
corelib.WriteJsonResponse(w, 500, "参数错误", nil)
}
if err = corelib.DbMysql.Limit(pageSize).Offset(offset).Find(&user).Error; err != nil {
corelib.Logger.Error("UserPageApiHandler, all, 查询分页数据失败")
corelib.WriteJsonResponse(w, 500, "参数错误", nil)
}
corelib.Logger.Infoln("paginationData: ", paginationData)
numPage := math.Ceil(float64(total) / float64(pageSize))
if page > int(numPage) {
page = int(numPage)
}
paginationData := corelib.GetPaginationData(int(numPage), page, 2, total)
res := model.UserpageResponse{}
res.PaginationData = paginationData
if total > 0 {
for _, v := range user {
res.Data = append(res.Data, model.UserResponse{
ID: v.ID,
Username: v.Username,
IsSuper: v.IsSuper,
Remark: v.Remark,
CreatedAt: v.CreatedAt,
UpdatedAt: v.UpdatedAt,
})
}
}
corelib.WriteJsonResponse(w, http.StatusOK, "", res)
} else {
corelib.WriteJsonResponse(w, 405, "方法错误", nil)
@ -128,6 +169,54 @@ func UserPageApiHandler(w http.ResponseWriter, r *http.Request) {
}
}
func pagination(page, onePageCount int, search string) {
func pagination(page, pageSize int, search string) (model.UserpageResponse, error) {
var (
err error
user []model.SysUser
total int64
)
if page < 1 {
page = 1
}
offset := (page - 1) * pageSize
if search != "" {
if err = corelib.DbMysql.Where("name like ?", "%"+search+"%").Find(&model.SysUser{}).Count(&total).Error; err != nil {
corelib.Logger.Error("UserPageApiHandler, search, 查询total失败", err)
return model.UserpageResponse{}, errors.New("数据查询失败")
}
if err = corelib.DbMysql.Limit(pageSize).Offset(offset).Where("name like ?", "%"+search+"%").Find(&user).Error; err != nil {
corelib.Logger.Error("UserPageApiHandler, search, 查询分页数据失败", err)
return model.UserpageResponse{}, errors.New("数据查询失败")
}
} else {
if err = corelib.DbMysql.Find(&model.SysUser{}).Count(&total).Error; err != nil {
corelib.Logger.Error("UserPageApiHandler, all, 查询total失败", err)
return model.UserpageResponse{}, errors.New("数据查询失败")
}
if err = corelib.DbMysql.Limit(pageSize).Offset(offset).Find(&user).Error; err != nil {
corelib.Logger.Error("UserPageApiHandler, all, 查询分页数据失败", err)
return model.UserpageResponse{}, errors.New("数据查询失败")
}
}
numPage := math.Ceil(float64(total) / float64(pageSize))
if page > int(numPage) {
page = int(numPage)
}
paginationData := corelib.GetPaginationData(int(numPage), page, 2, total)
res := model.UserpageResponse{}
res.PaginationData = paginationData
if total > 0 {
for _, v := range user {
res.Data = append(res.Data, model.UserResponse{
ID: v.ID,
Username: v.Username,
IsSuper: v.IsSuper,
Remark: v.Remark,
CreatedAt: v.CreatedAt,
UpdatedAt: v.UpdatedAt,
})
}
}
return res, nil
}

View File

@ -2,6 +2,7 @@ package middleware
import (
"context"
"github.com/gorilla/sessions"
"net/http"
"picgo/configs"
"picgo/corelib"
@ -13,24 +14,32 @@ import (
// LoginMiddleware 登录 // 添加日志中间件到路由器 使用r.Handle("/", LoginMiddleware(http.HandlerFunc(handler)))
func LoginMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
user model.SysUser
err error
session *sessions.Session
)
resPath := r.URL.Path
if resPath == "/login" || resPath == "/captcha" || strings.HasPrefix(resPath, "/static") {
next.ServeHTTP(w, r)
}
session, _ := corelib.SessionStore.Get(r, configs.Settings.Server.SessionName)
if session, err = corelib.SessionStore.Get(r, configs.Settings.Server.SessionName); err == nil {
}
username, ok := session.Values["username"].(string)
if !ok || username == "" {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
var (
user model.SysUser
err error
)
if user, err = data.SysUserSelectByUsername(username); err != nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// 权限判断
if user.IsSuper != 1 && (resPath != "/" && resPath != "/api/v1/upload") {
http.Error(w, "没有权限访问", 403)
return
}
ctx := context.WithValue(r.Context(), "username", user.Username)
next.ServeHTTP(w, r.WithContext(ctx))
})

20
model/user_response.go Normal file
View File

@ -0,0 +1,20 @@
package model
import (
"picgo/corelib"
"time"
)
type UserpageResponse struct {
corelib.PaginationData
Data []UserResponse
}
type UserResponse struct {
ID int64 `gorm:"primary_key;unique;not null;comment:'主键'" json:"id"`
CreatedAt time.Time `gorm:"not null;type:timestamp;default:CURRENT_TIMESTAMP;comment:'创建时间'"`
UpdatedAt time.Time `gorm:"not null;type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'"`
Username string `gorm:"size:32;unique;not null;column:username;comment:'用户名'" json:"username"`
IsSuper int `gorm:"type:TINYINT(1);default:0;column:is_super;comment:'是否是超级管理员-0:否,1:是'" json:"is_super"`
Remark string `gorm:"size:64;column:remark;comment:'备注'" json:"remark"`
}

View File

@ -38,6 +38,7 @@ func InitRouter() *mux.Router {
// 需要鉴权接口路由
r.Handle("/api/v1/upload", middleware.LoginMiddleware(http.HandlerFunc(handler.UploadFileApiHandler))).Methods(http.MethodPost) // 图片上传接口
r.Handle("/api/v1/user/create", middleware.LoginMiddleware(http.HandlerFunc(handler.UserCreateApiHandler))).Methods(http.MethodPost) // 新增用户
r.Handle("/api/v1/user/page", middleware.LoginMiddleware(http.HandlerFunc(handler.UserPageApiHandler))).Methods(http.MethodPost) // 分页查询用户
// 应用 CORS CSRF 中间件
http.Handle("/", middleware.CorsMiddleware(CSRF(r)))

View File

@ -42,6 +42,18 @@ Common.prototype.AlertConfirm = function (params) {
})
}
Common.prototype.AlertSimpleWarn = function (msg) {
if (!msg) {
msg = '失败!'
}
let $warn = $('<div class="alert alert-danger alert-danger-simple" style="position: absolute; top:60px; right: 50%; z-index: 999; display: block;" role="alert">')
$warn.append($('<span>' + msg + '</span>'))
$("#body-box-top").append($warn)
setTimeout(function () {
$('.alert-danger-simple').remove()
}, 3000);
}
// 成功弹窗
Common.prototype.AlertSuccessToast = function (msg) {
if (!msg) {
@ -50,6 +62,4 @@ Common.prototype.AlertSuccessToast = function (msg) {
this.AlertToast(msg, 'success')
}
var Alert = new Common()
var Alert = new Common()

View File

@ -1,18 +1,20 @@
function User() {
this.pageSize = 10
this.page = 1
this.search = ""
}
User.prototype.CreateUser = function () {
// 示例字段配置
let fields = [
{ label: '用户名:', name: 'username', type: 'text' },
{ label: '密码:', name: 'password', type: 'password' },
{ label: '确认密码:', name: 'confirm', type: 'password' }
{label: '用户名:', name: 'username', type: 'text'},
{label: '密码:', name: 'password', type: 'password'},
{label: '确认密码:', name: 'confirm', type: 'password'}
]
// 初始化表单并显示
$('#add-user').on('click', function() {
$('#add-user').on('click', function () {
let title = '添加用户'
let modal = new Modal(title, fields,function(data) {
let modal = new Modal(title, fields, function (data) {
console.log('表单提交数据:', data)
let csrfToken = document.getElementsByName("gorilla.csrf.Token")[0].value
console.log('表单提交数据csrfToken', csrfToken)
@ -29,7 +31,6 @@ User.prototype.CreateUser = function () {
Alert.AlertError("请确认密码!")
return
}
Alert.AlertSuccessToast("创建用户成功!")
$.ajax({
url: '/api/v1/user/create',
method: 'POST',
@ -44,7 +45,8 @@ User.prototype.CreateUser = function () {
success: function (result) {
if (result.status === 200) {
Alert.AlertSuccessToast(result.message)
location.reload();
// 刷新表格
} else {
Alert.AlertError(result.message)
}
@ -60,13 +62,37 @@ User.prototype.CreateUser = function () {
}
User.prototype.GetUserData = function () {
User.prototype.RefreshTable = function () {
let self = this
let csrfToken = document.getElementsByName("gorilla.csrf.Token")[0].value
let url = `/api/v1/user/page?pageSize=${self.pageSize}&page=${self.page}`
if (!self.search) {
url = `/api/v1/user/page?pageSize=${self.pageSize}&page=${self.page}&search=${self.search}`
}
$.ajax({
url: url,
method: 'POST',
headers: {
'Content-Type': 'application/json',
"X-CSRF-Token": csrfToken
},
success: function (result) {
if (result.status === 200) {
console.log("用户数据: ", result.data)
} else {
Alert.AlertSimpleWarn(result.message)
}
},
error: function (xhr, status, error) {
Alert.AlertError("服务器内部错误!")
console.log(" 状态: ", status, ' 请求失败:', error);
}
})
}
User.prototype.run = function () {
this.RefreshTable()
this.CreateUser()
}

View File

@ -15,7 +15,7 @@
{{template "style" .}}
<title>{{.Title}}</title>
</head>
<body>
<body id="body-box-top">
<div class="container-fluid bg-dark" style="padding-left: 0; padding-right: 0">
<div class="container" style="padding-left: 0; padding-right: 0">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">

View File

@ -7,7 +7,106 @@
<button type="button" class="btn btn-primary" id="add-user"><i class="bi bi-plus-square"></i> 创建用户</button>
</div>
</div>
<div id="user-table"></div>
<div id="user-table">
<table class="table table-bordered">
<thead>
<tr class="table-primary">
<th scope="col">ID</th>
<th scope="col">用户名</th>
<th scope="col">用户权限</th>
<th scope="col">备注</th>
<th scope="col">创建时间</th>
<th scope="col">更新时间</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody id="tbody-user">
{{ range .Data.Data }}
<tr>
<td>{{.ID}}</td>
<td>{{.Username}}</td>
{{ if .IsSuper }}
<td>
<span class="badge badge-success">超级管理</span>
</td>
{{ else }}
<td>
<span class="badge badge-secondary">普通用户</span>
</td>
{{end}}
<td>{{.Remark}}</td>
<td>{{.CreatedAt.Format "2006-01-02 15:04:05"}}</td>
<td>{{.UpdatedAt.Format "2006-01-02 15:04:05"}}</td>
<td>操作</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div id="user-pagination" class="form-inline">
<!-- <nav aria-label="Page navigation">-->
<!-- <ul class="pagination">-->
<!-- <li class="page-item">-->
<!-- <div class="btn-group">-->
<!-- <button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">-->
<!-- 10-->
<!-- </button>-->
<!-- <div class="dropdown-menu">-->
<!-- <a class="dropdown-item" href="#">10</a>-->
<!-- <a class="dropdown-item" href="#">20</a>-->
<!-- <a class="dropdown-item" href="#">30</a>-->
<!-- </div>-->
<!-- </div>-->
<!-- </li>-->
<!-- <li class="page-item">-->
<!-- <a class="page-link" href="#" aria-label="Previous">-->
<!-- <span aria-hidden="true">&laquo;</span>-->
<!-- </a>-->
<!-- </li>-->
<!-- <li class="page-item"><a class="page-link" href="#">1</a></li>-->
<!-- <li class="page-item"><a class="page-link" href="#">2</a></li>-->
<!-- <li class="page-item"><a class="page-link" href="#">3</a></li>-->
<!-- <li class="page-item">-->
<!-- <a class="page-link" href="#" aria-label="Next">-->
<!-- <span aria-hidden="true">&raquo;</span>-->
<!-- </a>-->
<!-- </li>-->
<!-- </ul>-->
<!-- </nav>-->
<!-- 分页导航 -->
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm">
<li class="page-item mr-2">
<select class="form-control" style="max-height: 31px; line-height: 31px; padding: 0 0 0 6px;">
<option value="10" selected>10条/页</option>
<option value="20">20条/页</option>
<option value="50">50条/页</option>
</select>
</li>
<li class="page-item{{if eq .Data.CurrentPage 1}} disabled{{end}}">
<a class="page-link" href="javascript:void(0);" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a>
</li>
<li class="page-item"><a class="page-link" href="javascript:void(0);" data-page="1">1</a></li>
<li class="page-item active"><a class="page-link" href="javascript:void(0);" data-page="2">2</a></li>
<li class="page-item"><a class="page-link" href="javascript:void(0);" data-page="3">3</a></li>
<!-- 省略部分页码 -->
<li class="page-item disabled"><span class="page-link">...</span></li>
<li class="page-item"><a class="page-link" href="javascript:void(0);" data-page="9">9</a></li>
<li class="page-item"><a class="page-link" href="javascript:void(0);" data-page="10">10</a></li>
<li class="page-item {{if eq .Data.CurrentPage .Data.NumPages}} disabled{{end}}">
<a class="page-link" href="javascript:void(0);" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled ml-2"><span class="page-link" style="border: none">到第</span></li>
<li class="page-item"><input type="text" class="form-control" style="max-width: 40px;max-height: 31px"></li>
<li class="page-item ml-1 disabled"><span class="page-link" style="border: none"></span></li>
<li class="page-item ml-2"><a class="page-link" href="javascript:void(0);">确定</a></li>
<li class="page-item disabled ml-2"><span class="page-link" style="border: none">共 {{.Data.Total}} 条</span></li>
</ul>
</nav>
</div>
{{end}}
{{define "script"}}