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 }