惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Security Archives - TechRepublic
Security Archives - TechRepublic
P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
P
Palo Alto Networks Blog
Know Your Adversary
Know Your Adversary
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cisco Talos Blog
Cisco Talos Blog
L
Lohrmann on Cybersecurity
AWS News Blog
AWS News Blog
J
Java Code Geeks
博客园_首页
Scott Helme
Scott Helme
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
T
The Exploit Database - CXSecurity.com
Security Latest
Security Latest
V
Visual Studio Blog
Cloudbric
Cloudbric
Jina AI
Jina AI
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
A
Arctic Wolf
C
Cybersecurity and Infrastructure Security Agency CISA
S
SegmentFault 最新的问题
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
W
WeLiveSecurity
K
Kaspersky official blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hacker News: Ask HN
Hacker News: Ask HN
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
量子位
Google Online Security Blog
Google Online Security Blog
博客园 - Franky
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 三生石上(FineUI控件)
Recent Commits to openclaw:main
Recent Commits to openclaw:main

博客园 - 刘俊涛的博客

qoder-体验分享 50+专家角色+多代理框架,一个人就能建起一支AI 公司 小米突然放大招!2亿 Token 免费领,最后几天冲刺! 这个项目把浏览器“干了30年的事”干掉了:Pretext 为什么突然爆火? 通用商品打标模块实现方案(基于位运算,适配多框架多语言) 后端工程中使用 Model + Trait 设计可复用的数据模型(完整实现示例) 最小阻力之路 U盘打不开 : 文件损坏 : 提示需要格式化?用CHKDSK轻松修复教程 祂看顧麻雀(His Eye Is on the Sparrow)鄭漢良 别再把一切都变成数组了:用迭代器让 JavaScript 既快又省心 Spark性能调优实战笔记 Spark神操作:轻松拿捏公司“历史烂数据”,一人干翻十年脏表 特定血型血液生成的研究进展:化学合成与微生物生产技术路线 非暴力沟通 PHP 8.1+ 引入的 枚举(Enum) 类型 PHP 8.5 新特性速览:管道操作符、clone with、闭包增强与更多实用功能 从错误中学习 产品拆解到底怎么做?8步框架,新手产品经理必学干货! 《创作与爱:托芙·扬松传》读书笔记 MongoDB 端口安全加固方案 为什么软件反应特别慢?一次因版本架构错误导致的性能问题排查记录 【Go 语言神器】iota 到底是什么?为什么高手都爱用它?
禁止直接 update 业务关键字段
刘俊涛的博客 · 2026-02-09 · via 博客园 - 刘俊涛的博客

禁止直接 update 业务关键字段

三条核心规则(核心字段只允许状态驱动修改所有关键变更写审计表禁止直接 update 业务关键字段)在 PHP 中实现的完整生产级示例。

我们以一个典型的订单系统为例,核心字段包括 status(订单状态),这是典型的业务关键字段。

技术栈假设

  • PHP 8.1+
  • MySQL
  • 使用 PDO 进行数据库操作(推荐)
  • 简单的领域模型 + 服务层设计(不依赖框架,也便于迁移到 Laravel/Symfony)

1. 数据库表结构

-- 订单主表
CREATE TABLE orders (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_no VARCHAR(50) NOT NULL UNIQUE,
    user_id BIGINT UNSIGNED NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status TINYINT NOT NULL DEFAULT 1 COMMENT '1=待支付,2=已支付,3=已发货,4=已完成,5=已取消',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 订单审计表
CREATE TABLE order_audit_logs (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT UNSIGNED NOT NULL,
    field_name VARCHAR(50) NOT NULL,
    old_value VARCHAR(255) NULL,
    new_value VARCHAR(255) NULL,
    operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID',
    operation VARCHAR(50) NOT NULL COMMENT '如 change_status',
    reason VARCHAR(255) NULL COMMENT '变更原因',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_order_id (order_id)
);

2. 订单状态枚举(状态机定义)

<?php
// src/Enum/OrderStatus.php
namespace App\Enum;

enum OrderStatus: int
{
    case PendingPayment = 1;  // 待支付
    case Paid           = 2;  // 已支付
    case Shipped        = 3;  // 已发货
    case Completed      = 4;  // 已完成
    case Cancelled      = 5;  // 已取消

    // 定义允许的状态流转规则(状态机)
    public function canTransitionTo(self $newStatus): bool
    {
        return match ($this) {
            self::PendingPayment => in_array($newStatus, [self::Paid, self::Cancelled]),
            self::Paid           => in_array($newStatus, [self::Shipped, self::Cancelled]),
            self::Shipped        => $newStatus === self::Completed,
            self::Completed      => false,
            self::Cancelled      => false,
            default              => false,
        };
    }

    public function getName(): string
    {
        return match ($this) {
            self::PendingPayment => '待支付',
            self::Paid           => '已支付',
            self::Shipped        => '已发货',
            self::Completed      => '已完成',
            self::Cancelled      => '已取消',
        };
    }
}

3. 订单实体(核心字段只允许通过方法修改)

<?php
// src/Entity/Order.php
namespace App\Entity;

use App\Enum\OrderStatus;

class Order
{
    private int $id;
    private string $orderNo;
    private int $userId;
    private float $amount;
    private OrderStatus $status;

    public function __construct(int $id, string $orderNo, int $userId, float $amount, OrderStatus $status)
    {
        $this->id = $id;
        $this->orderNo = $orderNo;
        $this->userId = $userId;
        $this->amount = $amount;
        $this->status = $status;
    }

    public function getId(): int { return $this->id; }
    public function getOrderNo(): string { return $this->orderNo; }
    public function getStatus(): OrderStatus { return $this->status; }

    // 禁止外部直接修改状态,只能通过 changeStatus 方法(状态驱动)
    public function changeStatus(OrderStatus $newStatus, string $reason = ''): void
    {
        if (!$this->status->canTransitionTo($newStatus)) {
            throw new \DomainException(
                sprintf('订单[%s] 当前状态[%s] 不能变更为[%s]', 
                    $this->orderNo, 
                    $this->status->getName(), 
                    $newStatus->getName()
                )
            );
        }

        $this->status = $newStatus;
        // 注意:这里不直接写数据库,由服务层统一处理审计和持久化
    }
}

4. 审计日志服务

<?php
// src/Service/OrderAuditService.php
namespace App\Service;

use PDO;

class OrderAuditService
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function log(
        int $orderId,
        string $fieldName,
        ?string $oldValue,
        ?string $newValue,
        int $operatorId,
        string $operation,
        ?string $reason = null
    ): void {
        $sql = "INSERT INTO order_audit_logs 
                (order_id, field_name, old_value, new_value, operator_id, operation, reason) 
                VALUES (:order_id, :field, :old, :new, :operator, :op, :reason)";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            ':order_id'  => $orderId,
            ':field'     => $fieldName,
            ':old'       => $oldValue,
            ':new'       => $newValue,
            ':operator'  => $operatorId,
            ':op'        => $operation,
            ':reason'    => $reason,
        ]);
    }
}

5. 订单服务层(核心业务逻辑 + 审计 + 禁止直接UPDATE)

<?php
// src/Service/OrderService.php
namespace App\Service;

use App\Entity\Order;
use App\Enum\OrderStatus;
use PDO;
use PDOException;

class OrderService
{
    private PDO $pdo;
    private OrderAuditService $auditService;

    public function __construct(PDO $pdo, OrderAuditService $auditService)
    {
        $this->pdo = $pdo;
        $this->auditService = $auditService;
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }

    // 加载订单(从数据库)
    public function getOrderById(int $orderId): Order
    {
        $stmt = $this->pdo->prepare("SELECT * FROM orders WHERE id = :id");
        $stmt->execute([':id' => $orderId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$row) {
            throw new \Exception("订单不存在");
        }

        return new Order(
            $row['id'],
            $row['order_no'],
            $row['user_id'],
            (float)$row['amount'],
            OrderStatus::from($row['status'])
        );
    }

    // 关键变更:修改订单状态(状态驱动 + 审计)
    public function changeOrderStatus(
        int $orderId,
        OrderStatus $newStatus,
        int $operatorId,
        string $reason = ''
    ): void {
        $this->pdo->beginTransaction();
        try {
            $order = $this->getOrderById($orderId);

            $oldStatus = $order->getStatus();

            // 状态驱动修改(唯一允许修改状态的入口)
            $order->changeStatus($newStatus, $reason);

            // 更新主表(仅此一处允许 UPDATE status)
            $sql = "UPDATE orders SET status = :status WHERE id = :id";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([
                ':status' => $newStatus->value,
                ':id'     => $orderId
            ]);

            // 写入审计日志(关键变更必审)
            $this->auditService->log(
                orderId: $orderId,
                fieldName: 'status',
                oldValue: $oldStatus->value . '(' . $oldStatus->getName() . ')',
                newValue: $newStatus->value . '(' . $newStatus->getName() . ')',
                operatorId: $operatorId,
                operation: 'change_status',
                reason: $reason
            );

            $this->pdo->commit();
        } catch (\Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }

    // 其他业务方法...(支付、发货等都类似封装)
}

6. 使用示例(控制器或脚本)

<?php
require_once 'autoload.php'; // 假设有简单的自动加载

$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', '', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

$auditService = new App\Service\OrderAuditService($pdo);
$orderService = new App\Service\OrderService($pdo, $auditService);

try {
    // 示例:操作员ID为1001的用户将订单ID=1的状态改为“已支付”
    $orderService->changeOrderStatus(
        orderId: 1,
        newStatus: \App\Enum\OrderStatus::Paid,
        operatorId: 1001,
        reason: '用户完成微信支付'
    );

    echo "订单状态修改成功并已记录审计日志\n";
} catch (\Exception $e) {
    echo "操作失败: " . $e->getMessage() . "\n";
}

总结:如何满足三条规则

规则 如何实现
核心字段只允许状态驱动修改 使用 OrderStatus 枚举 + canTransitionTo() + 实体内 changeStatus() 方法强制校验
所有关键变更写审计表 OrderAuditService 在每次关键变更后插入 order_audit_logs
禁止直接 update 业务关键字段 所有外部代码只能通过 OrderService::changeOrderStatus() 修改状态,禁止裸 SQL UPDATE

这种设计在生产环境中非常稳健、可审计、可扩展,后续可轻松迁移到 Laravel(使用 Eloquent 事件 + Policy)或 Symfony(使用 Doctrine 事件)中保持相同约束。

如需加入更多字段(如金额退款、地址变更)的审计,只需扩展类似模式即可。

Don’t reinvent the wheel, library code is there to help.

欢迎关注公-众-号【TaonyDaily】、留言、评论,一起学习。