Files
qitongxue-api/internal/logic/admin_audit.go
2026-09-29 10:57:01 +08:00

143 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"context"
"encoding/json"
"strconv"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
v1 "tool-api/api/admin/v1"
"tool-api/internal/consts"
)
// auditId 审计对象主键统一转字符串(避免与 note.go 的 itoa(int) 冲突)
func auditId(v int64) string { return strconv.FormatInt(v, 10) }
// ============================================================================
// 后台操作审计(T17)
//
// WriteAudit 只负责「落一条记录」,调用点负责提供前后值;写入失败仅告警,
// 绝不因审计异常影响主流程(审计是旁路,不是业务前置条件)。
// ============================================================================
// AuditEntry 一条待写入的操作审计
type AuditEntry struct {
Action string // 动作,如 user.set_level / event.status / admin.create
TargetType string // 对象类型:user/event/table/admin/export...
TargetId string // 对象主键(字符串,兼容复合键)
Before interface{} // 操作前值(可为 nil / map / struct)
After interface{} // 操作后值
Result int // consts.AuditResultSuccess / Fail
Remark string
}
// auditJSON 把前后值序列化成 JSON 字符串;nil 存 NULL。
func auditJSON(v interface{}) interface{} {
if v == nil {
return nil
}
b, err := json.Marshal(v)
if err != nil {
return nil
}
return string(b)
}
func auditClientIp(ctx context.Context) string {
req := g.RequestFromCtx(ctx)
if req == nil {
return ""
}
return req.GetClientIp()
}
// WriteAudit 写入一条操作审计(操作人取自上下文,由 AdminPerm 中间件注入)。
func WriteAudit(ctx context.Context, e AuditEntry) {
data := g.Map{
"admin_id": CtxAdminId(ctx),
"admin_account": CtxAdminAccount(ctx),
"admin_name": CtxAdminName(ctx),
"role_value": CtxAdminRole(ctx),
"action": e.Action,
"target_type": e.TargetType,
"target_id": e.TargetId,
"before_json": auditJSON(e.Before),
"after_json": auditJSON(e.After),
"result": e.Result,
"remark": e.Remark,
"ip": auditClientIp(ctx),
"created_at": gtime.Now(),
}
if _, err := g.Model(consts.TableAdminAuditLog).Data(data).Insert(); err != nil {
g.Log().Warningf(ctx, "[audit] 写入失败 action=%s err=%v", e.Action, err)
}
}
// AdminAuditList 审计列表(多条件筛选 + 分页)。
func AdminAuditList(ctx context.Context, req *v1.AuditListReq) (*v1.AuditListRes, error) {
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
m := g.Model(consts.TableAdminAuditLog)
if req.AdminKeyword != "" {
m = m.WhereLike("admin_account", "%"+req.AdminKeyword+"%")
}
if req.Action != "" {
m = m.WhereLike("action", "%"+req.Action+"%")
}
if req.Result != nil {
m = m.Where("result", *req.Result)
}
if req.DateFrom != "" {
m = m.WhereGTE("created_at", req.DateFrom+" 00:00:00")
}
if req.DateTo != "" {
m = m.WhereLTE("created_at", req.DateTo+" 23:59:59")
}
total, err := m.Count()
if err != nil {
return nil, err
}
records, err := m.Page(req.Page, req.PageSize).OrderDesc("id").All()
if err != nil {
return nil, err
}
list := make([]v1.AuditItem, 0, len(records))
for _, r := range records {
list = append(list, v1.AuditItem{
Id: r["id"].Int64(),
AdminId: r["admin_id"].Int64(),
AdminAccount: r["admin_account"].String(),
AdminName: r["admin_name"].String(),
RoleValue: r["role_value"].String(),
Action: r["action"].String(),
TargetType: r["target_type"].String(),
TargetId: r["target_id"].String(),
BeforeJson: r["before_json"].String(),
AfterJson: r["after_json"].String(),
Result: r["result"].Int(),
Remark: r["remark"].String(),
Ip: r["ip"].String(),
CreatedAt: r["created_at"].String(),
})
}
return &v1.AuditListRes{List: list, Total: total}, nil
}
// AdminAuditRecord 前端主动上报一条审计(如「导出」为纯前端动作,服务端无法感知)。
func AdminAuditRecord(ctx context.Context, req *v1.AuditRecordReq) (*v1.AuditRecordRes, error) {
WriteAudit(ctx, AuditEntry{
Action: req.Action,
TargetType: req.TargetType,
TargetId: req.TargetId,
Result: consts.AuditResultSuccess,
Remark: req.Remark,
})
return &v1.AuditRecordRes{}, nil
}