包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
132 lines
3.5 KiB
Go
132 lines
3.5 KiB
Go
package admin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
|
"github.com/gothinkster/golang-gin-realworld-example-app/middleware"
|
|
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
|
)
|
|
|
|
// PaginationParams holds parsed ProTable pagination query params
|
|
type PaginationParams struct {
|
|
Current int
|
|
PageSize int
|
|
}
|
|
|
|
// ParsePagination reads ?current=N&pageSize=M from the request
|
|
func ParsePagination(c *gin.Context) PaginationParams {
|
|
current, _ := strconv.Atoi(c.DefaultQuery("current", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
|
if current < 1 {
|
|
current = 1
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
return PaginationParams{Current: current, PageSize: pageSize}
|
|
}
|
|
|
|
// Offset returns the DB offset for GORM queries
|
|
func (p PaginationParams) Offset() int {
|
|
return (p.Current - 1) * p.PageSize
|
|
}
|
|
|
|
// OK responds with {success: true, data: ...}
|
|
func OK(c *gin.Context, data interface{}) {
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
|
|
}
|
|
|
|
// OKList responds with {success: true, data: [...], total: N} — used by ProTable
|
|
func OKList(c *gin.Context, data interface{}, total int64) {
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": data, "total": total})
|
|
}
|
|
|
|
// Fail responds with {success: false, message: ...}
|
|
func Fail(c *gin.Context, status int, msg string) {
|
|
c.JSON(status, gin.H{"success": false, "message": msg})
|
|
}
|
|
|
|
// flexTimeLayouts is the set of time formats accepted from the frontend.
|
|
var flexTimeLayouts = []string{
|
|
"2006-01-02T15:04:05",
|
|
"2006-01-02 15:04:05",
|
|
"2006-01-02T15:04",
|
|
"2006-01-02 15:04",
|
|
"2006-01-02",
|
|
}
|
|
|
|
// BindJSONFlexTime works like c.ShouldBindJSON but also accepts non-RFC3339
|
|
// time strings such as "2026-02-28 00:00:00" that Ant Design's DatePicker sends.
|
|
// It reads the raw body, converts any date-like strings to RFC3339, then unmarshals.
|
|
func BindJSONFlexTime(c *gin.Context, obj interface{}) error {
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal(body, &raw); err != nil {
|
|
return json.Unmarshal(body, obj) // fallback: let normal errors surface
|
|
}
|
|
|
|
for key, val := range raw {
|
|
var s string
|
|
if err := json.Unmarshal(val, &s); err != nil {
|
|
continue // not a string, skip
|
|
}
|
|
for _, layout := range flexTimeLayouts {
|
|
if t, err := time.Parse(layout, s); err == nil {
|
|
converted, _ := json.Marshal(t) // produces RFC3339
|
|
raw[key] = converted
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
fixed, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(fixed, obj)
|
|
}
|
|
|
|
// LogOp writes a record to operation_logs table
|
|
func LogOp(c *gin.Context, opType, targetType, action string, targetID *uint, payload interface{}) {
|
|
db := common.GetDB()
|
|
user, _ := middleware.GetCurrentUser(c)
|
|
|
|
var userID *uint
|
|
if user != nil {
|
|
uid := uint(user.UserID)
|
|
userID = &uid
|
|
}
|
|
|
|
var changesStr string
|
|
if payload != nil {
|
|
b, _ := json.Marshal(payload)
|
|
changesStr = string(b)
|
|
}
|
|
|
|
db.Create(&models.OperationLog{
|
|
UserID: userID,
|
|
OperationType: opType,
|
|
TargetType: targetType,
|
|
Action: action,
|
|
TargetID: targetID,
|
|
Changes: changesStr,
|
|
IPAddress: c.ClientIP(),
|
|
UserAgent: c.GetHeader("User-Agent"),
|
|
Status: "success",
|
|
CreatedAt: time.Now(),
|
|
})
|
|
}
|