AI 生成后台改数据后,操作日志别只记按钮:Go + MySQL 怎么验

AI 生成后台改数据后,操作日志别只记按钮:Go + MySQL 怎么验

AI 生成的后台页面能新增、编辑、删除,并不代表上线后能追责。真正出问题时,最麻烦的不是接口返回 200,而是你只知道"有人改过",不知道谁改了哪一行、从什么值改到什么值、当时命中了哪个权限码、请求来自哪个租户或角色。

很多后台系统把操作日志做成按钮旁边的一句 记录日志,或者只在前端路由里记"用户点击了编辑"。这种日志看起来有内容,排障时基本用不上。前端按钮可以绕过,批量接口不会走同一个点击事件,AI 生成代码还常把新增、编辑、删除都塞进同一个通用 handler。最后数据库里只有一行"编辑客户",没有目标 ID,没有前后差异,也没有接口权限。

这篇只讨论一个更窄的问题:Go 后台里,AI 生成 CRUD 之后,操作日志应该怎么验才算能用。重点不是做一个漂亮的日志列表,而是把权限、数据变更和数据库写入放在同一条链路里检查。

先看一个容易漏的表结构

假设有一张客户表,AI 生成 CRUD 时通常会给出类似结构:

sql 复制代码
CREATE TABLE crm_customer (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  name VARCHAR(80) NOT NULL,
  phone VARCHAR(32) DEFAULT '',
  level VARCHAR(20) DEFAULT 'normal',
  owner_user_id BIGINT NOT NULL,
  remark VARCHAR(255) DEFAULT '',
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL,
  deleted_at DATETIME NULL,
  KEY idx_tenant_owner (tenant_id, owner_user_id),
  KEY idx_tenant_deleted (tenant_id, deleted_at)
);

页面上点"编辑",保存成功,只能证明更新语句执行了。操作日志至少还要回答这些问题:

问题 不能只靠什么判断 必须落到哪里
谁改的 前端用户名 后端登录态里的 user_id
改了哪条 页面当前行 请求参数和数据库主键
改了什么 "编辑客户"文案 变更前后的字段差异
有没有权限 菜单是否可见 接口权限码和后端中间件
属于哪个租户 前端筛选条件 SQL 的 tenant_id 条件

这几个字段缺一个,排障都会很难受。尤其是 tenant_idpermission_code,它们不该靠前端传进来。

操作日志表不要只存一个 message

一个可查的操作日志表,建议先把检索维度拆出来。JSON 可以放差异,但关键字段不要全塞进 JSON。

sql 复制代码
CREATE TABLE admin_operation_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  actor_id BIGINT NOT NULL,
  actor_name VARCHAR(64) NOT NULL,
  permission_code VARCHAR(100) NOT NULL,
  module VARCHAR(64) NOT NULL,
  action VARCHAR(32) NOT NULL,
  target_table VARCHAR(64) NOT NULL,
  target_id BIGINT NOT NULL,
  before_json JSON NULL,
  after_json JSON NULL,
  diff_json JSON NULL,
  request_id VARCHAR(64) NOT NULL,
  ip VARCHAR(64) DEFAULT '',
  user_agent VARCHAR(255) DEFAULT '',
  result VARCHAR(20) NOT NULL,
  error_msg VARCHAR(255) DEFAULT '',
  created_at DATETIME NOT NULL,
  KEY idx_tenant_target (tenant_id, target_table, target_id),
  KEY idx_actor_time (actor_id, created_at),
  KEY idx_perm_time (permission_code, created_at),
  KEY idx_request_id (request_id)
);

这里有两个细节。

第一,before_jsonafter_json 最好来自数据库读回,不要直接拿前端提交的 body 当 before。前端只知道它看到的字段,后端才知道真实保存了什么。

第二,permission_code 不是装饰字段。它要和后端权限中间件命中的权限码一致。否则日志里写的是 customer:edit,实际接口只校验了登录态,还是没法证明权限链路没漏。

Go 接口里先读旧值,再写新值

下面是一个简化版写法,省掉框架细节,只保留顺序。真正项目里可以放到 service 或 middleware,但不要把"读旧值"和"写日志"完全丢给前端。

go 复制代码
type CustomerPatch struct {
    Name   string `json:"name"`
    Phone  string `json:"phone"`
    Level  string `json:"level"`
    Remark string `json:"remark"`
}

type AuditLog struct {
    TenantID       int64
    ActorID        int64
    ActorName      string
    PermissionCode string
    Module         string
    Action         string
    TargetTable    string
    TargetID       int64
    BeforeJSON     string
    AfterJSON      string
    DiffJSON       string
    RequestID      string
    Result         string
    ErrorMsg       string
}

func UpdateCustomer(ctx context.Context, id int64, req CustomerPatch) error {
    tenantID := TenantIDFromCtx(ctx)
    actor := ActorFromCtx(ctx)
    perm := "crm_customer:update"

    oldRow, err := repo.GetCustomerForUpdate(ctx, tenantID, id)
    if err != nil {
        writeAudit(ctx, AuditLog{
            TenantID: tenantID, ActorID: actor.ID, ActorName: actor.Name,
            PermissionCode: perm, Module: "crm_customer", Action: "update",
            TargetTable: "crm_customer", TargetID: id,
            RequestID: RequestIDFromCtx(ctx), Result: "failed", ErrorMsg: err.Error(),
        })
        return err
    }

    next := oldRow
    next.Name = req.Name
    next.Phone = req.Phone
    next.Level = req.Level
    next.Remark = req.Remark

    diff := BuildDiff(oldRow, next, []string{"name", "phone", "level", "remark"})
    if len(diff) == 0 {
        return nil
    }

    if err := repo.UpdateCustomer(ctx, tenantID, id, next); err != nil {
        writeAudit(ctx, AuditLog{
            TenantID: tenantID, ActorID: actor.ID, ActorName: actor.Name,
            PermissionCode: perm, Module: "crm_customer", Action: "update",
            TargetTable: "crm_customer", TargetID: id,
            BeforeJSON: ToJSON(oldRow), DiffJSON: ToJSON(diff),
            RequestID: RequestIDFromCtx(ctx), Result: "failed", ErrorMsg: err.Error(),
        })
        return err
    }

    writeAudit(ctx, AuditLog{
        TenantID: tenantID, ActorID: actor.ID, ActorName: actor.Name,
        PermissionCode: perm, Module: "crm_customer", Action: "update",
        TargetTable: "crm_customer", TargetID: id,
        BeforeJSON: ToJSON(oldRow), AfterJSON: ToJSON(next), DiffJSON: ToJSON(diff),
        RequestID: RequestIDFromCtx(ctx), Result: "success",
    })
    return nil
}

这段代码的重点不是结构体写法,而是顺序:权限上下文先确定,旧值从数据库拿,更新和日志共用同一个 tenant_idtarget_idrequest_id。如果 AI 只生成了 db.Update(req),没有旧值、没有 diff、没有失败日志,就要补验收项。

权限中间件和操作日志要对得上

后台最常见的假安全是:按钮隐藏了,接口也有鉴权,但日志里看不出当时鉴权的是哪一个权限码。排查时只能翻代码。

验收时可以用三条 curl 覆盖:未登录、无权限、有权限。

bash 复制代码
# 1. 未登录,应该 401,不能写 success 日志
curl -i -X PATCH 'https://api.example.com/admin/crm/customers/1001' \
  -H 'Content-Type: application/json' \
  -d '{"level":"vip"}'

# 2. 登录但没有 crm_customer:update,应该 403,日志可记录 failed + permission_code
curl -i -X PATCH 'https://api.example.com/admin/crm/customers/1001' \
  -H 'Authorization: Bearer staff_without_perm' \
  -H 'Content-Type: application/json' \
  -d '{"level":"vip"}'

# 3. 有权限,应该 200,并能查到 diff_json
curl -i -X PATCH 'https://api.example.com/admin/crm/customers/1001' \
  -H 'Authorization: Bearer manager_with_perm' \
  -H 'Content-Type: application/json' \
  -d '{"level":"vip","remark":"signed"}'

对应的 SQL 也要能查出来:

sql 复制代码
SELECT actor_id, permission_code, target_table, target_id, result, diff_json, request_id
FROM admin_operation_log
WHERE tenant_id = 10
  AND target_table = 'crm_customer'
  AND target_id = 1001
ORDER BY id DESC
LIMIT 5;

期望结果不是"有一行日志"这么简单,而是能看到类似内容:

json 复制代码
{
  "actor_id": 23,
  "permission_code": "crm_customer:update",
  "target_table": "crm_customer",
  "target_id": 1001,
  "result": "success",
  "diff_json": {
    "level": {"before": "normal", "after": "vip"},
    "remark": {"before": "", "after": "signed"}
  },
  "request_id": "req_20260727_113001"
}

如果这里只剩 message=编辑客户成功,后续遇到脏数据就很难追。

批量更新要按目标行写日志

AI 生成批量接口时,容易把日志写成一条:批量更新成功,共 30 条。这条日志对统计有用,对追责没用。批量接口至少要有批次日志和行级日志两层。

sql 复制代码
CREATE TABLE admin_operation_batch (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  actor_id BIGINT NOT NULL,
  action VARCHAR(32) NOT NULL,
  target_table VARCHAR(64) NOT NULL,
  request_id VARCHAR(64) NOT NULL,
  total_count INT NOT NULL,
  success_count INT NOT NULL,
  failed_count INT NOT NULL,
  created_at DATETIME NOT NULL,
  KEY idx_request_id (request_id)
);

批次表回答"这次操作整体发生了什么",行级日志回答"每一行发生了什么"。客户等级、负责人、状态这类字段,一旦批量改错,不能只靠回忆 Excel 原文件来恢复。

批量接口的验收 SQL 可以这样写:

sql 复制代码
SELECT COUNT(*) AS row_logs
FROM admin_operation_log
WHERE request_id = 'req_bulk_20260727_01'
  AND target_table = 'crm_customer'
  AND action = 'batch_update';

SELECT success_count, failed_count
FROM admin_operation_batch
WHERE request_id = 'req_bulk_20260727_01';

如果批次里成功 18 条、失败 2 条,行级日志就应该能对应到 20 个目标 ID。只记录一个总数,不够。

数据库约束失败也要进日志

操作日志不是只记录成功。很多线上问题恰恰发生在失败路径:唯一索引冲突、租户条件不匹配、状态不允许修改、字段超长、外键不存在。失败不写日志,用户只会看到"保存失败",后台没有证据。

例如手机号唯一约束冲突:

sql 复制代码
ALTER TABLE crm_customer
ADD UNIQUE KEY uk_tenant_phone_active (tenant_id, phone, deleted_at);

更新时撞到唯一索引,日志里至少保留目标 ID、权限码、请求字段和错误码:

json 复制代码
{
  "tenant_id": 10,
  "actor_id": 23,
  "permission_code": "crm_customer:update",
  "target_table": "crm_customer",
  "target_id": 1001,
  "result": "failed",
  "error_msg": "Duplicate entry '10-13800000000' for key 'uk_tenant_phone_active'",
  "request_id": "req_20260727_113201"
}

这里不要把完整手机号、身份证、token 这类敏感字段原样打满。日志要能排障,也要能脱敏。手机号可以存 138****0000,token 不该进日志。

和代码生成器的关系:不要只生成接口,也要生成验收点

代码生成器很容易生成列表、新增、编辑、删除这些基础文件,难点是把权限码和日志字段一起纳入模板边界。我看 Go 后台项目时,会重点看生成器有没有把"接口、权限、菜单、日志"放到同一张验收表里。

XYGo Admin 里可以直接看两个位置:server/internal/middleware/operation_log.go 负责操作日志中间件,server/internal/middleware/admin_permission.go 负责后台接口权限校验。它们能作为这篇判断的源码样本:日志不是单独的装饰功能,应该贴着权限和请求上下文走。源码入口放这里就够了:GitHub 仓库

如果你维护的是自己的生成器,也可以把下面这张表放进模板验收:

模板项 验收方式
update 接口 读旧值、写新值、生成 diff
delete 接口 记录目标 ID、租户、权限码、结果
batch 接口 批次日志 + 行级日志
permission 中间件 日志里的 permission_code 与后端校验一致
request_id 接口响应、应用日志、操作日志能串起来
脱敏 手机号、token、身份证不进明文日志

上线前按这几步查

我的检查顺序一般是这样:

bash 复制代码
# 1. 找出最近 10 条客户修改日志
SELECT id, actor_id, permission_code, target_id, result, created_at
FROM admin_operation_log
WHERE target_table = 'crm_customer'
ORDER BY id DESC
LIMIT 10;

# 2. 查某个 request_id 是否能串到应用日志
SELECT request_id, diff_json, error_msg
FROM admin_operation_log
WHERE request_id = 'req_20260727_113001';

# 3. 查有没有成功修改但 diff 为空的日志
SELECT id, target_id, action, result
FROM admin_operation_log
WHERE result = 'success'
  AND action IN ('update', 'batch_update')
  AND (diff_json IS NULL OR JSON_LENGTH(diff_json) = 0)
ORDER BY id DESC
LIMIT 20;

# 4. 查有没有缺权限码的日志
SELECT id, module, action, target_id
FROM admin_operation_log
WHERE permission_code = '' OR permission_code IS NULL
ORDER BY id DESC
LIMIT 20;

前两条用于确认链路能追,后两条用于找生成代码留下的洞。尤其是第三条,很多"保存成功但没实际变化"的请求会制造噪音。如果没有字段变化,可以不写成功日志,或者写成 noop,别混进真正的更新记录里。

结论

AI 生成后台时,操作日志不能只当页面功能。它更像后台系统的黑匣子:平时没人看,出问题时必须能还原现场。

最小可用标准就四个:后端权限码能对上,目标行能定位,字段差异能读懂,失败路径也有记录。只要这四个没过,页面再顺也别急着上线。后面真遇到"谁把客户等级改了""为什么普通账号能改负责人""批量导入覆盖了哪几行",日志能不能回答,差别很大。

相关推荐
糖果店的幽灵1 小时前
我用 Codex + Remotion,做了一条唐朝纸片分层动画
人工智能
KaMeidebaby1 小时前
卡梅德生物技术快报 | 核酸适配体文库测序:核酸适配体文库测序的技术原理、实验流程与数据解析
前端·网络·数据库·人工智能·算法
阿里云大数据AI技术2 小时前
AI 时代,零售商超如何用多模态数据"看见"每一排货架?
人工智能
一RTOS一2 小时前
【无标题】
人工智能·鸿道实时操作系统·国产嵌入式操作系统选型·智算控一体
cian_2 小时前
我把 Anthropic 官方的 Claude Cookbook 拆成了 30+ 个能直接跑的例子
人工智能
ClouGence2 小时前
除了 DBeaver,2026 年还有哪些好用的开源数据库管理工具?
数据库·开源
秦先生在广东2 小时前
`claude-video /watch`:给 Claude 装上“眼睛“看视频的工程实现与边界分析
人工智能
极客猴子2 小时前
会议记录APP怎么选 2026年实测功能对比与使用指南
人工智能·xcode
秦先生在广东2 小时前
Impeccable:给 AI 编码 Agent 装上设计判断力的工具包
人工智能