Skip to main content

sz_rust_orm_ext_facade/
model.rs

1//! 模型模块 — BaseModel trait + Append 字段系统
2//!
3//! 对齐 PHP `think\Model` + `app\common\model\szoa\BaseModel`,委托 SZ-ORM `Model`。
4//!
5//! ## PHP 对齐
6//!
7//! | PHP 属性/方法 | Rust 等价 | 说明 |
8//! |---------------|----------|------|
9//! | `$name` | [`Model::table_name()`] | 表名 |
10//! | `$pk` | [`Model::pk_name()`] + [`Model::pk()`] | 主键列名 + 主键值 |
11//! | `$append` | [`BaseModel::append()`] | 追加虚拟字段 |
12//! | `getXxxAttr` | [`BaseModel::get_appended_value()`] | 访问器(虚拟字段值) |
13//! | `$field`/`$fillable` | [`ModelExt::fillable()`] | 可批量赋值字段 |
14//! | `$disuse`/`$guarded` | [`ModelExt::guarded()`] | 受保护字段 |
15//! | `$hidden` | [`ModelExt::hidden()`] | 序列化时隐藏字段 |
16//! | `$visible` | [`ModelExt::visible()`] | 序列化时白名单字段 |
17//! | `$type` | [`ModelExt::casts()`] | 字段类型转换 |
18//! | `save()` | Repository 层 | 持久化由 Repository 提供(见架构说明) |
19//! | `delete()` | Repository 层 | 持久化由 Repository 提供 |
20//! | `startTrans()`/`commit()`/`rollback()` | Repository 层 | 事务由 Repository 提供 |
21//!
22//! ## 架构决策:Model 与 Repository 分离
23//!
24//! PHP `think\Model` 采用 Active Record 模式,Model 既是数据载体也是持久化行为。
25//! Rust SZ-ORM 采用 Data Mapper + Repository 模式:
26//! - [`BaseModel`] trait 只描述模型元数据(表名/主键/字段/append)
27//! - 持久化动作(save/delete/事务)由 `sz_orm_core::repository::Repository` 提供
28//!
29//! 这符合 Rust 的设计哲学:分离数据描述与副作用行为,避免 Model trait 承载过多职责。
30//! 业务模型通过实现 [`BaseModel`] trait 获得元数据描述能力,
31//! 通过注入 `Repository` 获得持久化能力。
32//!
33//! ## Append 字段系统
34//!
35//! 对齐 PHP `$append = ['status_text']` + `getStatusTextAttr($value, $data)`:
36//!
37//! ```ignore
38//! use sz_rust_core::model::BaseModel;
39//! use serde_json::json;
40//!
41//! struct Customer {
42//!     customer_id: i64,
43//!     status: i32,
44//! }
45//!
46//! impl BaseModel for Customer {
47//!     fn append() -> Vec<&'static str> {
48//!         vec!["status_text"]
49//!     }
50//!
51//!     fn get_appended_value(&self, field: &str) -> Option<serde_json::Value> {
52//!         match field {
53//!             "status_text" => Some(json!(match self.status {
54//!                 0 => "禁用",
55//!                 1 => "启用",
56//!                 _ => "未知",
57//!             })),
58//!             _ => None,
59//!         }
60//!     }
61//! }
62//! ```
63
64#![forbid(unsafe_code)]
65
66use serde_json::Value;
67use std::collections::HashMap;
68use sz_rust_orm_facade::{Model, ModelExt, RelationLoader};
69
70/// BaseModel trait — 对齐 PHP `app\common\model\szoa\BaseModel`
71///
72/// 组合 SZ-ORM 的 [`Model`] + [`ModelExt`] + [`RelationLoader`],并补充:
73/// 1. **Append 字段系统**(对齐 PHP `$append` + `getXxxAttr`)
74/// 2. **带 append 的序列化**([`Self::to_json_with_append`])
75///
76/// ## 元数据属性继承
77///
78/// BaseModel 通过组合 SZ-ORM trait 自动获得以下能力:
79/// - **表名**:[`Model::table_name()`](对齐 PHP `$name`)
80/// - **主键**:[`Model::pk_name()`] + [`Model::pk()`](对齐 PHP `$pk`)
81/// - **可填充字段**:[`ModelExt::fillable()`](对齐 PHP `$field`/`$fillable`)
82/// - **受保护字段**:[`ModelExt::guarded()`](对齐 PHP `$disuse`/`$guarded`)
83/// - **隐藏字段**:[`ModelExt::hidden()`](对齐 PHP `$hidden`)
84/// - **可见字段**:[`ModelExt::visible()`](对齐 PHP `$visible`)
85/// - **类型转换**:[`ModelExt::casts()`](对齐 PHP `$type`)
86///
87/// ## 持久化方法
88///
89/// PHP `save()`/`delete()`/`startTrans()`/`commit()`/`rollback()` 由
90/// `sz_orm_core::repository::Repository` 提供,不在 BaseModel trait 中定义。
91/// 详见模块文档「架构决策:Model 与 Repository 分离」。
92pub trait BaseModel: Model + ModelExt + RelationLoader + Send + Sync + 'static {
93    // ==================== Append 字段系统 ====================
94
95    /// 追加字段列表(对齐 PHP `$append = ['status_text']`)
96    ///
97    /// 序列化时自动追加这些虚拟字段,配合 [`Self::get_appended_value()`] 提供具体值。
98    ///
99    /// ## 默认实现
100    ///
101    /// 返回空 Vec(无追加字段),业务模型按需重写。
102    fn append() -> Vec<&'static str> {
103        Vec::new()
104    }
105
106    /// 获取追加字段的值(对齐 PHP `getXxxAttr($value, $data)`)
107    ///
108    /// 默认返回 `None`。业务模型重写此方法,根据当前模型数据计算虚拟字段值。
109    ///
110    /// ## 参数
111    ///
112    /// - `field`:字段名(来自 [`Self::append()`] 列表)
113    ///
114    /// ## 返回
115    ///
116    /// - `Some(Value)`:字段值
117    /// - `None`:字段不存在或无法计算
118    fn get_appended_value(&self, _field: &str) -> Option<Value> {
119        None
120    }
121
122    /// 序列化为 JSON(包含 append 字段)
123    ///
124    /// 先调用 [`ModelExt::to_json()`] 获取基础 JSON,然后追加 [`Self::append()`]
125    /// 中定义的虚拟字段(通过 [`Self::get_appended_value()`] 获取值)。
126    ///
127    /// ## 字段顺序
128    ///
129    /// 基础字段在前,append 字段在后(对齐 PHP `array_merge` 行为)。
130    /// append 字段之间的顺序由 [`Self::append()`] 返回的 Vec 顺序决定。
131    ///
132    /// ## PHP 行为对齐
133    ///
134    /// - append 字段**始终输出**(无访问器返回 `null`,对齐 PHP `Conversion.php` 第 292 行
135    ///   `$item[$name] = $this->getAttr($name)`,`getAttr` 无访问器时返回 `null`)
136    /// - append 字段**绕过 hidden 过滤**(PHP bug 复刻:`appendAttrToArray` 直接赋值,
137    ///   不检查 `$hidden`,见 `Conversion.php` 第 291-296 行)
138    ///
139    /// ## 缓存说明
140    ///
141    /// 此方法走 `get_appended_value`(独立路径,**不带访问器缓存**)。
142    /// 若需走访问器缓存(对齐 PHP `getAttr` 缓存机制),请实现 [`Appendable`] trait
143    /// 并使用 [`Appendable::to_json_with_append_cached()`]。
144    fn to_json_with_append(&self) -> Value {
145        let mut json = self.to_json();
146        if let Value::Object(ref mut map) = json {
147            for field in Self::append() {
148                // PHP 行为:append 字段始终输出(None → null)
149                let value = self.get_appended_value(field).unwrap_or(Value::Null);
150                map.insert(field.to_string(), value);
151            }
152        }
153        json
154    }
155}
156
157// ============================================================================
158// 访问器 / 修改器系统 — 对齐 PHP `getAttr` / `setAttr` / `getXxxAttr` / `setXxxAttr`
159//
160// PHP 源码依据:
161// - `vendor/topthink/think-orm/src/model/concern/Attribute.php` 第 367-540 行
162// - 命名规则:`Str::studly($name)` → `getXxxAttr` / `setXxxAttr`
163// - 访问器缓存:`$this->get[$fieldName]`,修改器失效同名字段缓存
164// - 修改器 null + data 已修改 → 提前返回(Attribute.php 第 379-381 行)
165// - 修改器第二参数 = `array_merge($this->data, $data)`
166// - 访问器优先于 `$type` 类型转换
167// ============================================================================
168
169/// 修改器返回结果 — 对齐 PHP `setXxxAttr` 返回值语义
170///
171/// PHP `setXxxAttr($value, $data)` 返回值的三种情况:
172/// 1. 返回非 null 值 → 写入 `$this->data[$name]`
173/// 2. 返回 null 且未修改 `$this->data` → 写入 null
174/// 3. 返回 null 且已修改 `$this->data` → 提前返回,不写入当前字段
175///
176/// Rust 中用 `MutatorResult` 显式表达:
177/// - `Value(v)`:对应情况 1
178/// - `Skip`:对应情况 3(修改器内部已通过 `data_map_mut` 修改 data)
179///
180/// 情况 2 在 Rust 中通过 `Some(MutatorResult::Value(Value::Null))` 表达。
181#[derive(Debug, Clone, PartialEq)]
182pub enum MutatorResult {
183    /// 修改器返回具体值,写入 `$this->data[$name]`
184    Value(Value),
185    /// 修改器返回 null 且内部已修改 data,跳过默认赋值
186    /// (对应 PHP `Attribute.php` 第 379-381 行提前返回分支)
187    Skip,
188}
189
190/// 访问器 trait — 对齐 PHP `getAttr` / `getXxxAttr`
191///
192/// ## PHP 对齐
193///
194/// | PHP 方法 | Rust 等价 | 说明 |
195/// |----------|----------|------|
196/// | `getAttr($name)` | [`Self::get_attr()`] | 入口方法,含缓存 |
197/// | `getData($name)` | [`Self::get_data()`] | 取原始字段值(不触发访问器) |
198/// | `getXxxAttr($value, $data)` | [`Self::accessor_for()`] | 业务模型按字段派发 |
199/// | `getRealFieldName($name)` | [`Self::real_field_name()`] | 字段名归一化 |
200/// | `__isset($name)` | [`Self::has_attr()`] | 触发访问器判 null |
201/// | `$this->data` | [`Self::data_map()`] | 原始字段数组 |
202/// | `$this->get` | [`Self::accessor_cache()`] | 访问器结果缓存 |
203///
204/// ## 缓存机制
205///
206/// - 首次 `get_attr(field)` 触发访问器,结果缓存到 `accessor_cache`
207/// - 同名字段被 `set_attr` 时失效对应缓存
208/// - **PHP bug 复刻**:不同名字段修改不失效派生字段缓存
209///   (如 `set_attr("status", ...)` 不失效 `status_text` 缓存)
210pub trait Accessor {
211    /// 取原始字段数组(对应 PHP `$this->data`)
212    fn data_map(&self) -> &HashMap<String, Value>;
213
214    /// 取可变原始字段数组
215    fn data_map_mut(&mut self) -> &mut HashMap<String, Value>;
216
217    /// 取访问器缓存(对应 PHP `$this->get`)
218    fn accessor_cache(&self) -> &HashMap<String, Value>;
219
220    /// 取可变访问器缓存
221    fn accessor_cache_mut(&mut self) -> &mut HashMap<String, Value>;
222
223    /// 字段名归一化(对应 PHP `getRealFieldName`)
224    ///
225    /// PHP 默认 `$strict=true, $convertNameToCamel=false` → 原样返回。
226    /// 业务模型按需重写以支持 snake_case ↔ camelCase 转换。
227    fn real_field_name(&self, name: &str) -> String {
228        name.to_string()
229    }
230
231    /// 业务模型重写:按字段名派发到具体访问器
232    ///
233    /// ## 参数
234    ///
235    /// - `field`:归一化后的字段名
236    /// - `value`:原始字段值(来自 `data_map`,可能为 `None`)
237    ///
238    /// ## 返回
239    ///
240    /// 访问器计算后的值(对应 PHP `getXxxAttr($value, $this->data)` 返回值)
241    ///
242    /// ## 默认实现
243    ///
244    /// 返回原始字段值(或 `Value::Null`),等价于 PHP 无访问器时走 `$type` / 关联 / 原值分支。
245    fn accessor_for(&self, field: &str, value: Option<&Value>) -> Value {
246        let _ = field;
247        value.cloned().unwrap_or(Value::Null)
248    }
249
250    /// 入口方法 — 对应 PHP `getAttr($name)`
251    ///
252    /// 执行流程(对齐 `Attribute.php` 第 497-540 行):
253    /// 1. 字段名归一化
254    /// 2. 缓存命中 → 直接返回
255    /// 3. 取原始字段值
256    /// 4. 派发到 `accessor_for`
257    /// 5. 写入缓存
258    fn get_attr(&mut self, name: &str) -> Value {
259        let field = self.real_field_name(name);
260
261        // 1. 缓存命中(对应 PHP $this->get[$fieldName])
262        if let Some(cached) = self.accessor_cache().get(&field) {
263            return cached.clone();
264        }
265
266        // 2. 取原始值(对应 PHP getData,不抛异常)
267        let value = self.data_map().get(&field);
268
269        // 3. 派发到具体访问器
270        let result = self.accessor_for(&field, value);
271
272        // 4. 写入缓存(对应 PHP $this->get[$fieldName] = $value)
273        self.accessor_cache_mut().insert(field, result.clone());
274
275        result
276    }
277
278    /// 取原始字段值(不触发访问器)— 对应 PHP `getData($name)`
279    fn get_data(&self, name: &str) -> Option<&Value> {
280        let field = self.real_field_name(name);
281        self.data_map().get(&field)
282    }
283
284    /// 检测字段是否存在 — 对应 PHP `__isset($name)`
285    ///
286    /// **PHP 行为复刻**:`isset($model->field)` 触发访问器执行并缓存结果。
287    fn has_attr(&mut self, name: &str) -> bool {
288        !self.get_attr(name).is_null()
289    }
290}
291
292/// 修改器 trait — 对齐 PHP `setAttr` / `setXxxAttr`
293///
294/// ## PHP 对齐
295///
296/// | PHP 方法 | Rust 等价 | 说明 |
297/// |----------|----------|------|
298/// | `setAttr($name, $value, $data)` | [`Self::set_attr()`] | 入口方法 |
299/// | `setXxxAttr($value, $data)` | [`Self::mutator_for()`] | 业务模型按字段派发 |
300/// | `setAttrs($data)` | [`Self::set_attrs()`] | 批量赋值 |
301///
302/// ## 修改器第二参数 `merged_data`
303///
304/// PHP `setXxxAttr($value, $data)` 中 `$data = array_merge($this->data, $data)`,
305/// 即「当前模型数据 + 外部批量数据」的合并。Rust 中 [`Self::mutator_for()`]
306/// 第三参数 `merged_data` 保留此语义。
307///
308/// ## 优先级
309///
310/// 1. 方法修改器 `mutator_for` → 若返回 `Some` 则使用其结果
311/// 2. 无修改器 → 原样写入 `data_map`
312///
313/// `$type` 类型转换 / 关联属性 / `__toString` 由各功能模块分别实现,当前阶段仅支持方法修改器。
314pub trait Mutator: Accessor {
315    /// 业务模型重写:按字段名派发到具体修改器
316    ///
317    /// ## 参数
318    ///
319    /// - `field`:归一化后的字段名
320    /// - `value`:被设置的值(引用,修改器按需 clone)
321    /// - `merged_data`:`data_map` + 外部 `data` 的合并(对应 PHP `array_merge`)
322    ///
323    /// ## 返回
324    ///
325    /// - `None`:无修改器,原样写入 data
326    /// - `Some(MutatorResult::Value(v))`:写入 `v` 到 data
327    /// - `Some(MutatorResult::Skip)`:跳过默认赋值(对应 PHP null + data modified 提前返回)
328    fn mutator_for(
329        &mut self,
330        field: &str,
331        value: &Value,
332        merged_data: &HashMap<String, Value>,
333    ) -> Option<MutatorResult>;
334
335    /// 入口方法 — 对应 PHP `setAttr($name, $value, $data)`
336    ///
337    /// 执行流程(对齐 `Attribute.php` 第 367-395 行):
338    /// 1. 字段名归一化
339    /// 2. 构造 `merged_data`(`data_map` + 外部 `data`)
340    /// 3. 派发到 `mutator_for`
341    /// 4. 处理 `Skip` / `Value` / `None` 三种结果
342    /// 5. 失效同名字段访问器缓存(对应 PHP `unset($this->get[$name])`)
343    fn set_attr(&mut self, name: &str, value: Value, data: Option<&HashMap<String, Value>>) {
344        let field = self.real_field_name(name);
345
346        // 1. 构造 merged_data(对应 PHP array_merge($this->data, $data))
347        let merged_data = if let Some(d) = data {
348            let mut m = self.data_map().clone();
349            m.extend(d.clone());
350            m
351        } else {
352            self.data_map().clone()
353        };
354
355        // 2. 派发到具体修改器
356        let result = self.mutator_for(&field, &value, &merged_data);
357
358        // 3. 处理结果
359        match result {
360            // PHP 第 379-381 行:修改器返回 null + 修改了 data → 提前返回
361            Some(MutatorResult::Skip) => {
362                self.accessor_cache_mut().remove(&field);
363            }
364            // 修改器返回具体值,写入 data
365            Some(MutatorResult::Value(v)) => {
366                self.data_map_mut().insert(field.clone(), v);
367                self.accessor_cache_mut().remove(&field);
368            }
369            // 无修改器,原样写入 data(对应 PHP 第 394 行 $this->data[$name] = $value)
370            None => {
371                self.data_map_mut().insert(field.clone(), value);
372                self.accessor_cache_mut().remove(&field);
373            }
374        }
375    }
376
377    /// 批量赋值 — 对应 PHP `setAttrs($data)`
378    ///
379    /// 对每个字段调用 `set_attr`,第三参数传完整 `data`(使修改器能感知批量上下文)。
380    fn set_attrs(&mut self, data: &HashMap<String, Value>) {
381        let fields: Vec<(String, Value)> =
382            data.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
383        for (field, value) in fields {
384            self.set_attr(&field, value, Some(data));
385        }
386    }
387}
388
389// ============================================================================
390// Append 字段系统 — 动态 append + 走访问器缓存
391//
392// PHP 源码依据:
393// - `vendor/topthink/think-orm/src/model/concern/Conversion.php` 第 88-97 行(append 方法)
394// - `vendor/topthink/think-orm/src/model/concern/Conversion.php` 第 234-236 行(toArray 中 append 处理)
395// - `vendor/topthink/think-orm/src/model/concern/Conversion.php` 第 291-296 行(appendAttrToArray 直接赋值,绕过 hidden)
396// - `vendor/topthink/think-orm/src/model/concern/Attribute.php` 第 475-486 行(getAttr 入口,带缓存)
397// ============================================================================
398
399/// 动态 Append 状态 — 对齐 PHP `$this->append` 实例属性
400///
401/// PHP 中 `$append` 是实例属性,可通过 `append()` 方法动态修改:
402/// - `append($fields)` 默认覆盖(`Conversion.php` 第 90-94 行)
403/// - `append($fields, true)` 合并(`Conversion.php` 第 91-93 行)
404///
405/// Rust 中通过 `AppendState` 持有动态状态,`None` 表示使用静态 [`BaseModel::append()`]。
406///
407/// ## 鲜视达项目实际用法
408///
409/// 项目内 12 个模型使用静态 `$append` 声明,**零动态 `->append()` 调用**。
410/// 此结构提供框架能力补全,业务模型按需使用。
411#[derive(Debug, Clone, Default)]
412pub struct AppendState {
413    /// 动态 append 字段列表(`None` 表示使用静态默认)
414    dynamic: Option<Vec<String>>,
415}
416
417impl AppendState {
418    /// 创建空状态(使用静态默认)
419    pub fn new() -> Self {
420        Self::default()
421    }
422
423    /// 覆盖动态 append(对齐 PHP `$model->append($fields)` 默认行为)
424    pub fn replace(&mut self, fields: Vec<String>) {
425        self.dynamic = Some(fields);
426    }
427
428    /// 合并到动态 append(对齐 PHP `$model->append($fields, true)`)
429    pub fn merge(&mut self, fields: Vec<String>) {
430        match &mut self.dynamic {
431            Some(existing) => {
432                for field in fields {
433                    if !existing.contains(&field) {
434                        existing.push(field);
435                    }
436                }
437            }
438            None => {
439                self.dynamic = Some(fields);
440            }
441        }
442    }
443
444    /// 获取动态 append 字段(如果有)
445    pub fn dynamic_fields(&self) -> Option<&Vec<String>> {
446        self.dynamic.as_ref()
447    }
448}
449
450/// Appendable trait — 对齐 PHP `append()` + `getAttr()` 统一派发
451///
452/// 业务模型实现此 trait 获得:
453/// 1. **动态 append 能力**(覆盖/合并,对齐 PHP `Conversion.php` 第 88-97 行)
454/// 2. **append 字段走 `get_attr` 缓存**(对齐 PHP `Conversion.php` 第 292 行 +
455///    `Attribute.php` 第 475-486 行)
456///
457/// ## 与 BaseModel 的关系
458///
459/// - [`BaseModel::to_json_with_append()`]:无缓存版本,走 `get_appended_value`(独立路径)
460/// - [`Self::to_json_with_append_cached()`]:带缓存版本,走 `get_attr`(统一路径,完全对齐 PHP)
461///
462/// 业务模型按需实现 `Appendable` 获得完整 PHP 对齐能力。
463///
464/// ## PHP 对齐
465///
466/// | PHP 方法 | Rust 等价 | 说明 |
467/// |----------|----------|------|
468/// | `$this->append` | [`AppendState`] | 动态 append 状态 |
469/// | `append($fields)` | [`Self::append_dyn()`] | 覆盖模式(默认) |
470/// | `append($fields, true)` | [`Self::append_merge()`] | 合并模式 |
471/// | `toArray()` 中 append 循环 | [`Self::to_json_with_append_cached()`] | 走 getAttr 缓存 |
472pub trait Appendable: BaseModel + Accessor {
473    /// 取动态 append 状态
474    fn append_state(&self) -> &AppendState;
475
476    /// 取可变动态 append 状态
477    fn append_state_mut(&mut self) -> &mut AppendState;
478
479    /// 动态 append(覆盖模式,对齐 PHP `$model->append($fields)`)
480    ///
481    /// 默认覆盖静态 [`BaseModel::append()`],返回 `&mut Self` 支持链式调用
482    ///(对齐 PHP `Conversion.php` 第 96 行 `return $this`)。
483    fn append_dyn(&mut self, fields: Vec<String>) -> &mut Self {
484        self.append_state_mut().replace(fields);
485        self
486    }
487
488    /// 动态 append(合并模式,对齐 PHP `$model->append($fields, true)`)
489    ///
490    /// PHP 语义(`Conversion.php` 第 91-93 行):`array_merge($this->append, $fields)`。
491    /// 首次合并时 `$this->append` 为静态默认值,需保留;后续合并累加到当前动态字段。
492    fn append_merge(&mut self, fields: Vec<String>) -> &mut Self {
493        if self.append_state().dynamic_fields().is_none() {
494            // 首次合并:初始化为「静态 append + fields」(去重)
495            let mut combined: Vec<String> = Self::append().iter().map(|s| s.to_string()).collect();
496            for field in fields {
497                if !combined.contains(&field) {
498                    combined.push(field);
499                }
500            }
501            self.append_state_mut().replace(combined);
502        } else {
503            // 已有动态字段:合并到现有
504            self.append_state_mut().merge(fields);
505        }
506        self
507    }
508
509    /// 获取生效的 append 字段列表
510    ///
511    /// 优先级:动态 append > 静态 [`BaseModel::append()`]
512    /// 对齐 PHP `$this->append`(动态覆盖后静态失效)
513    fn effective_append(&self) -> Vec<String> {
514        match self.append_state().dynamic_fields() {
515            Some(dyn_fields) => dyn_fields.clone(),
516            None => Self::append().iter().map(|s| s.to_string()).collect(),
517        }
518    }
519
520    /// 序列化为 JSON(包含 append 字段,走访问器缓存)
521    ///
522    /// 对齐 PHP `toArray()` 第 234-236 行 + `Attribute.php` `getAttr`:
523    /// 1. 先取基础 `to_json`(已应用 hidden 过滤)
524    /// 2. 对每个生效 append 字段调用 `get_attr`(带缓存)
525    /// 3. append 字段始终输出(无访问器返回 `null`,对齐 PHP 第 292 行)
526    /// 4. append 字段绕过 hidden 过滤(PHP bug 复刻,对齐第 291-296 行)
527    fn to_json_with_append_cached(&mut self) -> Value {
528        let mut json = self.to_json();
529        if let Value::Object(ref mut map) = json {
530            let fields = self.effective_append();
531            for field in fields {
532                // PHP 行为:append 字段始终走 getAttr(带缓存)
533                let value = self.get_attr(&field);
534                map.insert(field, value);
535            }
536        }
537        json
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544    use serde_json::json;
545    use std::collections::HashMap;
546    use sz_rust_orm_facade::Value as OrmValue;
547    use sz_rust_orm_facade::{Model, ModelExt, RelationLoader, TimestampFields};
548
549    // ====================================================================
550    // Mock 模型:无 append 字段
551    // ====================================================================
552
553    struct UserWithoutAppend {
554        user_id: i64,
555        username: String,
556        password: String,
557    }
558
559    impl Model for UserWithoutAppend {
560        type PrimaryKey = i64;
561
562        fn table_name() -> &'static str {
563            "sz_user"
564        }
565
566        fn pk_name() -> &'static str {
567            "user_id"
568        }
569
570        fn pk(&self) -> Self::PrimaryKey {
571            self.user_id
572        }
573
574        fn set_pk(&mut self, pk: Self::PrimaryKey) {
575            self.user_id = pk;
576        }
577
578        fn timestamp_fields() -> Option<TimestampFields> {
579            None
580        }
581
582        fn soft_delete_field() -> Option<&'static str> {
583            None
584        }
585    }
586
587    impl ModelExt for UserWithoutAppend {
588        fn columns() -> Vec<&'static str> {
589            vec!["user_id", "username", "password"]
590        }
591
592        fn fillable() -> Vec<&'static str> {
593            vec!["username", "password"]
594        }
595
596        fn guarded() -> Vec<&'static str> {
597            vec!["user_id"]
598        }
599
600        fn hidden() -> Vec<&'static str> {
601            vec!["password"]
602        }
603
604        fn get_column_value(&self, column: &str) -> Option<OrmValue> {
605            match column {
606                "user_id" => Some(OrmValue::I64(self.user_id)),
607                "username" => Some(OrmValue::String(self.username.clone())),
608                "password" => Some(OrmValue::String(self.password.clone())),
609                _ => None,
610            }
611        }
612
613        fn from_value(&mut self, _map: HashMap<String, OrmValue>) {
614            // 测试用:空实现
615        }
616    }
617
618    impl RelationLoader for UserWithoutAppend {
619        fn get_relation(&self, _name: &str) -> Option<&OrmValue> {
620            None
621        }
622
623        fn set_relation_data(&mut self, _name: &str, _data: OrmValue) {}
624
625        fn get_relation_fk_value(&self, _fk_name: &str) -> String {
626            String::new()
627        }
628    }
629
630    impl BaseModel for UserWithoutAppend {}
631
632    // ====================================================================
633    // Mock 模型:有 append 字段
634    // ====================================================================
635
636    struct CustomerWithAppend {
637        customer_id: i64,
638        status: i32,
639        name: String,
640    }
641
642    impl Model for CustomerWithAppend {
643        type PrimaryKey = i64;
644
645        fn table_name() -> &'static str {
646            "szoa_customer"
647        }
648
649        fn pk_name() -> &'static str {
650            "customer_id"
651        }
652
653        fn pk(&self) -> Self::PrimaryKey {
654            self.customer_id
655        }
656
657        fn set_pk(&mut self, pk: Self::PrimaryKey) {
658            self.customer_id = pk;
659        }
660    }
661
662    impl ModelExt for CustomerWithAppend {
663        fn columns() -> Vec<&'static str> {
664            vec!["customer_id", "status", "name"]
665        }
666
667        fn fillable() -> Vec<&'static str> {
668            vec!["status", "name"]
669        }
670
671        fn guarded() -> Vec<&'static str> {
672            vec!["customer_id"]
673        }
674
675        fn get_column_value(&self, column: &str) -> Option<OrmValue> {
676            match column {
677                "customer_id" => Some(OrmValue::I64(self.customer_id)),
678                "status" => Some(OrmValue::I32(self.status)),
679                "name" => Some(OrmValue::String(self.name.clone())),
680                _ => None,
681            }
682        }
683
684        fn from_value(&mut self, _map: HashMap<String, OrmValue>) {}
685    }
686
687    impl RelationLoader for CustomerWithAppend {
688        fn get_relation(&self, _name: &str) -> Option<&OrmValue> {
689            None
690        }
691
692        fn set_relation_data(&mut self, _name: &str, _data: OrmValue) {}
693
694        fn get_relation_fk_value(&self, _fk_name: &str) -> String {
695            String::new()
696        }
697    }
698
699    impl BaseModel for CustomerWithAppend {
700        fn append() -> Vec<&'static str> {
701            vec!["status_text"]
702        }
703
704        fn get_appended_value(&self, field: &str) -> Option<Value> {
705            match field {
706                "status_text" => Some(json!(match self.status {
707                    0 => "禁用",
708                    1 => "启用",
709                    _ => "未知",
710                })),
711                _ => None,
712            }
713        }
714    }
715
716    // ====================================================================
717    // 元数据属性测试(name/pk/fillable/guarded/hidden)
718    // ====================================================================
719
720    #[test]
721    fn test_base_model_table_name() {
722        // 对齐 PHP $name = 'customer'
723        assert_eq!(UserWithoutAppend::table_name(), "sz_user");
724        assert_eq!(CustomerWithAppend::table_name(), "szoa_customer");
725    }
726
727    #[test]
728    fn test_base_model_pk_name() {
729        // 对齐 PHP $pk = 'customer_id'
730        assert_eq!(UserWithoutAppend::pk_name(), "user_id");
731        assert_eq!(CustomerWithAppend::pk_name(), "customer_id");
732    }
733
734    #[test]
735    fn test_base_model_fillable() {
736        // 对齐 PHP $field/$fillable
737        assert_eq!(UserWithoutAppend::fillable(), vec!["username", "password"]);
738        assert_eq!(CustomerWithAppend::fillable(), vec!["status", "name"]);
739    }
740
741    #[test]
742    fn test_base_model_guarded() {
743        // 对齐 PHP $disuse/$guarded
744        assert_eq!(UserWithoutAppend::guarded(), vec!["user_id"]);
745        assert_eq!(CustomerWithAppend::guarded(), vec!["customer_id"]);
746    }
747
748    #[test]
749    fn test_base_model_hidden() {
750        // 对齐 PHP $hidden
751        assert_eq!(UserWithoutAppend::hidden(), vec!["password"]);
752        assert_eq!(CustomerWithAppend::hidden(), Vec::<&str>::new());
753    }
754
755    #[test]
756    fn test_base_model_pk_value() {
757        let user = UserWithoutAppend {
758            user_id: 42,
759            username: "alice".to_string(),
760            password: "secret".to_string(),
761        };
762        assert_eq!(user.pk(), 42);
763    }
764
765    // ====================================================================
766    // Append 字段系统测试
767    // ====================================================================
768
769    #[test]
770    fn test_base_model_append_default_empty() {
771        // 默认 append() 返回空 Vec
772        assert_eq!(UserWithoutAppend::append(), Vec::<&str>::new());
773    }
774
775    #[test]
776    fn test_base_model_append_with_status_text() {
777        // 对齐 PHP $append = ['status_text']
778        assert_eq!(CustomerWithAppend::append(), vec!["status_text"]);
779    }
780
781    #[test]
782    fn test_base_model_get_appended_value_default_none() {
783        let user = UserWithoutAppend {
784            user_id: 1,
785            username: "alice".to_string(),
786            password: "secret".to_string(),
787        };
788        // 默认 get_appended_value 返回 None
789        assert_eq!(user.get_appended_value("any_field"), None);
790    }
791
792    #[test]
793    fn test_base_model_get_appended_value_status_text() {
794        // 对齐 PHP getStatusTextAttr($value, $data)
795        let customer = CustomerWithAppend {
796            customer_id: 1,
797            status: 0,
798            name: "Alice Corp".to_string(),
799        };
800        assert_eq!(
801            customer.get_appended_value("status_text"),
802            Some(json!("禁用"))
803        );
804
805        let customer = CustomerWithAppend {
806            customer_id: 1,
807            status: 1,
808            name: "Alice Corp".to_string(),
809        };
810        assert_eq!(
811            customer.get_appended_value("status_text"),
812            Some(json!("启用"))
813        );
814
815        let customer = CustomerWithAppend {
816            customer_id: 1,
817            status: 99,
818            name: "Alice Corp".to_string(),
819        };
820        assert_eq!(
821            customer.get_appended_value("status_text"),
822            Some(json!("未知"))
823        );
824
825        // 未知字段返回 None
826        assert_eq!(customer.get_appended_value("unknown_field"), None);
827    }
828
829    // ====================================================================
830    // to_json_with_append 序列化测试
831    // ====================================================================
832
833    #[test]
834    fn test_base_model_to_json_without_append() {
835        // 无 append 的模型:to_json_with_append 等同于 to_json
836        let user = UserWithoutAppend {
837            user_id: 1,
838            username: "alice".to_string(),
839            password: "secret".to_string(),
840        };
841        let json = user.to_json_with_append();
842
843        // password 字段被 hidden 隐藏
844        assert_eq!(json["user_id"], 1);
845        assert_eq!(json["username"], "alice");
846        assert!(json.get("password").is_none(), "password 应被 hidden 隐藏");
847        assert!(json.get("status_text").is_none(), "无 append 字段");
848    }
849
850    #[test]
851    fn test_base_model_to_json_with_append() {
852        // 有 append 的模型:to_json_with_append 在基础字段后追加虚拟字段
853        let customer = CustomerWithAppend {
854            customer_id: 1,
855            status: 1,
856            name: "Alice Corp".to_string(),
857        };
858        let json = customer.to_json_with_append();
859
860        // 基础字段
861        assert_eq!(json["customer_id"], 1);
862        assert_eq!(json["status"], 1);
863        assert_eq!(json["name"], "Alice Corp");
864
865        // append 字段
866        assert_eq!(json["status_text"], "启用");
867    }
868
869    #[test]
870    fn test_base_model_to_json_append_field_order() {
871        // append 字段在基础字段之后
872        let customer = CustomerWithAppend {
873            customer_id: 1,
874            status: 0,
875            name: "Test".to_string(),
876        };
877        let json = customer.to_json_with_append();
878
879        if let Value::Object(map) = json {
880            let keys: Vec<&String> = map.keys().collect();
881            // 基础字段在前,append 字段在后
882            let customer_id_pos = keys.iter().position(|k| *k == "customer_id").unwrap();
883            let status_pos = keys.iter().position(|k| *k == "status").unwrap();
884            let name_pos = keys.iter().position(|k| *k == "name").unwrap();
885            let status_text_pos = keys.iter().position(|k| *k == "status_text").unwrap();
886
887            assert!(
888                customer_id_pos < status_text_pos,
889                "customer_id 应在 status_text 之前"
890            );
891            assert!(status_pos < status_text_pos, "status 应在 status_text 之前");
892            assert!(name_pos < status_text_pos, "name 应在 status_text 之前");
893        } else {
894            panic!("to_json_with_append 应返回 JSON Object");
895        }
896    }
897
898    // ====================================================================
899    // PHP 一致性测试(R5 硬约束:PHP/Rust 行为对比)
900    // ====================================================================
901
902    #[test]
903    fn test_php_consistency_model_name_aligns_php_name_property() {
904        // PHP: protected $name = 'customer';
905        // Rust: Model::table_name() 返回表名
906        assert_eq!(CustomerWithAppend::table_name(), "szoa_customer");
907    }
908
909    #[test]
910    fn test_php_consistency_model_pk_aligns_php_pk_property() {
911        // PHP: protected $pk = 'customer_id';
912        // Rust: Model::pk_name() 返回主键列名
913        assert_eq!(CustomerWithAppend::pk_name(), "customer_id");
914    }
915
916    #[test]
917    fn test_php_consistency_model_append_aligns_php_append_property() {
918        // PHP: protected $append = ['status_text'];
919        // Rust: BaseModel::append() 返回追加字段列表
920        assert_eq!(CustomerWithAppend::append(), vec!["status_text"]);
921    }
922
923    #[test]
924    fn test_php_consistency_model_hidden_aligns_php_hidden_property() {
925        // PHP: protected $hidden = ['password'];
926        // Rust: ModelExt::hidden() 返回隐藏字段列表
927        // 序列化时 password 字段不应出现
928        let user = UserWithoutAppend {
929            user_id: 1,
930            username: "alice".to_string(),
931            password: "secret".to_string(),
932        };
933        let json = user.to_json_with_append();
934        assert!(
935            json.get("password").is_none(),
936            "password 应被 hidden 隐藏(对齐 PHP $hidden)"
937        );
938    }
939
940    #[test]
941    fn test_php_consistency_get_xxx_attr_aligns_php_accessor() {
942        // PHP: getStatusTextAttr($value, $data) 根据状态返回文本
943        // Rust: get_appended_value("status_text") 返回对应的文本
944        let test_cases = vec![(0i32, "禁用"), (1, "启用"), (99, "未知")];
945
946        for (status, expected) in test_cases {
947            let customer = CustomerWithAppend {
948                customer_id: 1,
949                status,
950                name: "Test".to_string(),
951            };
952            assert_eq!(
953                customer.get_appended_value("status_text"),
954                Some(json!(expected)),
955                "status={} 应返回 '{}'",
956                status,
957                expected
958            );
959        }
960    }
961
962    #[test]
963    fn test_php_consistency_serialization_includes_append_fields() {
964        // PHP: 序列化时自动追加 $append 中的虚拟字段
965        // Rust: to_json_with_append() 在基础字段后追加 append 字段
966        let customer = CustomerWithAppend {
967            customer_id: 1,
968            status: 1,
969            name: "Alice Corp".to_string(),
970        };
971        let json = customer.to_json_with_append();
972
973        // 验证基础字段
974        assert_eq!(json["customer_id"], 1);
975        assert_eq!(json["status"], 1);
976        assert_eq!(json["name"], "Alice Corp");
977
978        // 验证 append 字段
979        assert_eq!(json["status_text"], "启用");
980    }
981
982    // ====================================================================
983    // 访问器 / 修改器系统测试
984    // ====================================================================
985
986    /// 测试用模型:实现 Accessor + Mutator
987    struct AccessorTestModel {
988        data: HashMap<String, Value>,
989        get_cache: HashMap<String, Value>,
990    }
991
992    impl AccessorTestModel {
993        fn new() -> Self {
994            Self {
995                data: HashMap::new(),
996                get_cache: HashMap::new(),
997            }
998        }
999
1000        fn with_data(mut self, key: &str, value: Value) -> Self {
1001            self.data.insert(key.to_string(), value);
1002            self
1003        }
1004    }
1005
1006    impl Accessor for AccessorTestModel {
1007        fn data_map(&self) -> &HashMap<String, Value> {
1008            &self.data
1009        }
1010
1011        fn data_map_mut(&mut self) -> &mut HashMap<String, Value> {
1012            &mut self.data
1013        }
1014
1015        fn accessor_cache(&self) -> &HashMap<String, Value> {
1016            &self.get_cache
1017        }
1018
1019        fn accessor_cache_mut(&mut self) -> &mut HashMap<String, Value> {
1020            &mut self.get_cache
1021        }
1022
1023        /// 模拟 PHP `getStatusTextAttr($value, $data)`
1024        /// - status=0 → "禁用"
1025        /// - status=1 → "启用"
1026        /// - 其他 → "未知"
1027        /// - 不存在的字段 → Value::Null(对齐 PHP $append 无访问器时返回 null)
1028        fn accessor_for(&self, field: &str, value: Option<&Value>) -> Value {
1029            match field {
1030                "status_text" => {
1031                    let status = self
1032                        .data
1033                        .get("status")
1034                        .and_then(|v| v.as_i64())
1035                        .unwrap_or(-1);
1036                    json!(match status {
1037                        0 => "禁用",
1038                        1 => "启用",
1039                        _ => "未知",
1040                    })
1041                }
1042                "rentarea_ids" => {
1043                    // 模拟 PHP getRentareaIdsAttr:逗号分隔字符串 → int 数组
1044                    let raw = value.and_then(|v| v.as_str()).unwrap_or("");
1045                    if raw.is_empty() {
1046                        json!([])
1047                    } else {
1048                        let arr: Vec<Value> = raw
1049                            .split(',')
1050                            .filter(|s| !s.is_empty())
1051                            .filter_map(|s| s.parse::<i64>().ok())
1052                            .map(Value::from)
1053                            .collect();
1054                        json!(arr)
1055                    }
1056                }
1057                "contract_price" => {
1058                    // 模拟 PHP getContractPriceAttr:float 强转,null → 0
1059                    // PHP (float)$value 会把字符串 "100.5" 转 100.5,Rust 需显式 parse
1060                    let price = value
1061                        .and_then(|v| {
1062                            if v.is_null() {
1063                                Some(0.0)
1064                            } else if let Some(f) = v.as_f64() {
1065                                Some(f)
1066                            } else if let Some(s) = v.as_str() {
1067                                s.parse::<f64>().ok()
1068                            } else {
1069                                None
1070                            }
1071                        })
1072                        .unwrap_or(0.0);
1073                    json!(price)
1074                }
1075                _ => value.cloned().unwrap_or(Value::Null),
1076            }
1077        }
1078    }
1079
1080    impl Mutator for AccessorTestModel {
1081        /// 模拟 PHP setRentareaIdsAttr:数组 → 逗号分隔字符串
1082        /// 模拟 PHP setSpecialAttr(返回 Skip):内部修改其他字段,跳过默认赋值
1083        fn mutator_for(
1084            &mut self,
1085            field: &str,
1086            value: &Value,
1087            merged_data: &HashMap<String, Value>,
1088        ) -> Option<MutatorResult> {
1089            match field {
1090                "rentarea_ids" => {
1091                    let arr: Vec<String> = match value {
1092                        Value::Array(items) => items
1093                            .iter()
1094                            .filter_map(|v| {
1095                                let s = match v {
1096                                    Value::String(s) => s.trim().to_string(),
1097                                    _ => v.to_string(),
1098                                };
1099                                if s.is_empty() {
1100                                    None
1101                                } else {
1102                                    Some(s)
1103                                }
1104                            })
1105                            .collect(),
1106                        _ => return Some(MutatorResult::Value(Value::String(String::new()))),
1107                    };
1108                    Some(MutatorResult::Value(Value::String(arr.join(","))))
1109                }
1110                "field_b" => {
1111                    // 模拟 PHP setFieldBAttr($value, $data):使用 merged_data 中的 field_a
1112                    let field_a = merged_data
1113                        .get("field_a")
1114                        .and_then(|v| v.as_i64())
1115                        .unwrap_or(0);
1116                    let val_b = value.as_i64().unwrap_or(0);
1117                    Some(MutatorResult::Value(json!(format!(
1118                        "{}_{}",
1119                        field_a, val_b
1120                    ))))
1121                }
1122                "special_attr" => {
1123                    // 模拟 PHP null + data modified 提前返回
1124                    self.data.insert("field_a".to_string(), json!("A"));
1125                    self.data.insert("field_b".to_string(), json!("B"));
1126                    Some(MutatorResult::Skip)
1127                }
1128                _ => None,
1129            }
1130        }
1131    }
1132
1133    #[test]
1134    fn test_accessor_basic_status_text() {
1135        // 对齐 PHP getStatusTextAttr($value, $data)
1136        let mut model = AccessorTestModel::new().with_data("status", json!(0));
1137        assert_eq!(model.get_attr("status_text"), json!("禁用"));
1138
1139        let mut model = AccessorTestModel::new().with_data("status", json!(1));
1140        assert_eq!(model.get_attr("status_text"), json!("启用"));
1141
1142        let mut model = AccessorTestModel::new().with_data("status", json!(99));
1143        assert_eq!(model.get_attr("status_text"), json!("未知"));
1144    }
1145
1146    #[test]
1147    fn test_accessor_real_field_value() {
1148        // 对齐 PHP getRentareaIdsAttr:逗号分隔字符串 → int 数组
1149        let mut model = AccessorTestModel::new().with_data("rentarea_ids", json!("1,2,3"));
1150        assert_eq!(model.get_attr("rentarea_ids"), json!([1, 2, 3]));
1151
1152        let mut model = AccessorTestModel::new().with_data("rentarea_ids", json!(""));
1153        assert_eq!(model.get_attr("rentarea_ids"), json!([]));
1154    }
1155
1156    #[test]
1157    fn test_accessor_float_coercion() {
1158        // 对齐 PHP getContractPriceAttr:null → 0.0,字符串数字 → float
1159        let mut model = AccessorTestModel::new().with_data("contract_price", Value::Null);
1160        assert_eq!(model.get_attr("contract_price"), json!(0.0));
1161
1162        let mut model = AccessorTestModel::new().with_data("contract_price", json!("100.5"));
1163        assert_eq!(model.get_attr("contract_price"), json!(100.5));
1164    }
1165
1166    #[test]
1167    fn test_accessor_cache_hit() {
1168        // 对齐 PHP $this->get[$fieldName] 缓存机制
1169        let mut model = AccessorTestModel::new().with_data("status", json!(1));
1170        let v1 = model.get_attr("status_text");
1171        // 修改 status 不失效 status_text 缓存(PHP bug 复刻)
1172        model.data.insert("status".to_string(), json!(0));
1173        let v2 = model.get_attr("status_text");
1174        assert_eq!(v1, v2, "缓存命中,访问器不重新执行");
1175        assert_eq!(v1, json!("启用"));
1176    }
1177
1178    #[test]
1179    fn test_accessor_cache_invalidation_on_set_same_field() {
1180        // 对齐 PHP unset($this->get[$name]):setAttr 同名字段失效缓存
1181        let mut model = AccessorTestModel::new().with_data("status", json!(1));
1182        let v1 = model.get_attr("status_text");
1183        assert_eq!(v1, json!("启用"));
1184
1185        // set_attr 同名字段(status),失效 status 缓存
1186        // 注意:status_text 缓存不受影响(PHP bug 复刻)
1187        model.set_attr("status", json!(0), None);
1188
1189        // 但 status_text 的缓存还在,不会重新计算
1190        let v2 = model.get_attr("status_text");
1191        assert_eq!(v2, json!("启用"), "status_text 缓存未失效(PHP bug 复刻)");
1192    }
1193
1194    #[test]
1195    fn test_mutator_basic_array_to_string() {
1196        // 对齐 PHP setRentareaIdsAttr:数组 → 逗号分隔字符串
1197        let mut model = AccessorTestModel::new();
1198        model.set_attr("rentarea_ids", json!([1, 2, 3]), None);
1199        assert_eq!(model.data.get("rentarea_ids"), Some(&json!("1,2,3")));
1200
1201        // 空数组
1202        model.set_attr("rentarea_ids", json!([]), None);
1203        assert_eq!(model.data.get("rentarea_ids"), Some(&json!("")));
1204    }
1205
1206    #[test]
1207    fn test_mutator_skip_php_bug_replication() {
1208        // 对齐 PHP Attribute.php 第 379-381 行:
1209        // 修改器返回 null + 已修改 data → 提前返回,不写入当前字段
1210        let mut model = AccessorTestModel::new();
1211        model.set_attr("special_attr", json!("X"), None);
1212
1213        // special_attr 未被写入
1214        assert!(
1215            !model.data.contains_key("special_attr"),
1216            "special_attr 应被跳过(PHP bug 复刻)"
1217        );
1218        // field_a / field_b 已被修改器写入
1219        assert_eq!(model.data.get("field_a"), Some(&json!("A")));
1220        assert_eq!(model.data.get("field_b"), Some(&json!("B")));
1221    }
1222
1223    #[test]
1224    fn test_mutator_merged_data() {
1225        // 对齐 PHP array_merge($this->data, $data):
1226        // 修改器第二参数是合并后的 data
1227        let mut model = AccessorTestModel::new();
1228        model.data.insert("field_a".to_string(), json!(1));
1229
1230        let mut batch = HashMap::new();
1231        batch.insert("field_a".to_string(), json!(100));
1232        batch.insert("field_b".to_string(), json!(2));
1233
1234        model.set_attrs(&batch);
1235
1236        // field_b 修改器使用 merged_data 中的 field_a=100(batch 覆盖 model.data)
1237        assert_eq!(model.data.get("field_b"), Some(&json!("100_2")));
1238    }
1239
1240    #[test]
1241    fn test_set_attrs_batch() {
1242        // 对齐 PHP setAttrs:批量赋值
1243        let mut model = AccessorTestModel::new();
1244        let mut batch = HashMap::new();
1245        batch.insert("status".to_string(), json!(1));
1246        batch.insert("name".to_string(), json!("Alice"));
1247
1248        model.set_attrs(&batch);
1249
1250        assert_eq!(model.data.get("status"), Some(&json!(1)));
1251        assert_eq!(model.data.get("name"), Some(&json!("Alice")));
1252    }
1253
1254    #[test]
1255    fn test_has_attr_triggers_accessor() {
1256        // 对齐 PHP __isset:触发访问器执行
1257        let mut model = AccessorTestModel::new().with_data("status", json!(1));
1258        assert!(model.has_attr("status_text"), "status_text 应存在");
1259
1260        // 无访问器且字段不存在 → 返回 Null → has_attr 返回 false
1261        assert!(!model.has_attr("nonexistent"), "不存在的字段应返回 false");
1262    }
1263
1264    #[test]
1265    fn test_get_data_returns_raw_value() {
1266        // 对齐 PHP getData:不触发访问器
1267        let model = AccessorTestModel::new().with_data("rentarea_ids", json!("1,2,3"));
1268        // get_data 返回原始字符串,不是数组
1269        assert_eq!(model.get_data("rentarea_ids"), Some(&json!("1,2,3")));
1270    }
1271
1272    #[test]
1273    fn test_accessor_for_unknown_field_returns_null() {
1274        // 对齐 PHP $append 字段无访问器时返回 null
1275        let mut model = AccessorTestModel::new();
1276        let v = model.get_attr("nonexistent_field");
1277        assert!(v.is_null(), "未知字段应返回 Null");
1278    }
1279
1280    #[test]
1281    fn test_real_field_name_default_identity() {
1282        // 对齐 PHP 默认 $strict=true, $convertNameToCamel=false:原样返回
1283        let model = AccessorTestModel::new();
1284        assert_eq!(model.real_field_name("status_text"), "status_text");
1285        assert_eq!(model.real_field_name("user_id"), "user_id");
1286    }
1287
1288    // ====================================================================
1289    // PHP 一致性测试(R5 硬约束:PHP/Rust 行为对比)
1290    // ====================================================================
1291
1292    #[test]
1293    fn test_php_consistency_accessor_cache_asymmetric_invalidation() {
1294        // PHP 行为:setAttr("status", ...) 不失效 status_text 缓存
1295        // 来源:Attribute.php 第 394 行 unset($this->get[$name]) 中 $name 是被 set 的字段名
1296        let mut model = AccessorTestModel::new().with_data("status", json!(1));
1297
1298        // 触发 status_text 访问器,缓存结果
1299        let v1 = model.get_attr("status_text");
1300        assert_eq!(v1, json!("启用"));
1301
1302        // 修改 status 字段
1303        model.set_attr("status", json!(0), None);
1304
1305        // 再次读取 status_text:缓存命中,仍是旧值
1306        let v2 = model.get_attr("status_text");
1307        assert_eq!(
1308            v2,
1309            json!("启用"),
1310            "PHP bug 复刻:status_text 缓存未失效,仍返回旧值"
1311        );
1312    }
1313
1314    #[test]
1315    fn test_php_consistency_mutator_skip_with_data_modification() {
1316        // PHP 行为:修改器返回 null + 已修改 data → 提前返回
1317        // 来源:Attribute.php 第 379-381 行
1318        let mut model = AccessorTestModel::new();
1319        model.set_attr("special_attr", json!("X"), None);
1320
1321        // 验证 PHP 行为:
1322        // 1. special_attr 未被写入
1323        assert!(
1324            !model.data.contains_key("special_attr"),
1325            "special_attr 应被跳过"
1326        );
1327        // 2. 修改器内部写入的 field_a / field_b 存在
1328        assert_eq!(model.data.get("field_a"), Some(&json!("A")));
1329        assert_eq!(model.data.get("field_b"), Some(&json!("B")));
1330    }
1331
1332    #[test]
1333    fn test_php_consistency_mutator_receives_merged_data() {
1334        // PHP 行为:setXxxAttr($value, array_merge($this->data, $data))
1335        // 来源:Attribute.php 第 377 行
1336        let mut model = AccessorTestModel::new();
1337        model.data.insert("field_a".to_string(), json!(1));
1338
1339        // 批量 setAttrs 时,field_b 修改器能读到 batch 中的 field_a
1340        let mut batch = HashMap::new();
1341        batch.insert("field_a".to_string(), json!(100));
1342        batch.insert("field_b".to_string(), json!(2));
1343        model.set_attrs(&batch);
1344
1345        // merged_data 中 field_a=100(batch 覆盖 model.data)
1346        // field_b 修改器返回 "100_2"
1347        assert_eq!(
1348            model.data.get("field_b"),
1349            Some(&json!("100_2")),
1350            "修改器应使用 merged_data 中的 field_a=100"
1351        );
1352    }
1353
1354    #[test]
1355    fn test_php_consistency_append_field_without_accessor_returns_null() {
1356        // PHP 行为:$append 字段无对应访问器 → 序列化输出 null
1357        // 来源:Conversion.php 第 280-296 行 + Attribute.php 第 525 行
1358        let mut model = AccessorTestModel::new();
1359        let v = model.get_attr("nonexistent_append_field");
1360        assert!(v.is_null(), "PHP 行为复刻:$append 字段无访问器应返回 null");
1361    }
1362
1363    #[test]
1364    fn test_php_consistency_isset_triggers_accessor() {
1365        // PHP 行为:__isset 触发访问器执行
1366        // 来源:Model.php 第 977-980 行
1367        let mut model = AccessorTestModel::new().with_data("status", json!(1));
1368
1369        // has_attr 应触发访问器,并缓存结果
1370        assert!(model.has_attr("status_text"));
1371
1372        // 验证缓存:再次 get_attr 应命中缓存(同值)
1373        let v = model.get_attr("status_text");
1374        assert_eq!(v, json!("启用"));
1375    }
1376
1377    #[test]
1378    fn test_php_consistency_accessor_overrides_raw_value() {
1379        // PHP 行为:访问器优先于原始值
1380        // 来源:Attribute.php 第 520-528 行
1381        // 真实字段 contract_price 的访问器把 null → 0.0
1382        let mut model = AccessorTestModel::new().with_data("contract_price", Value::Null);
1383
1384        // 原始值是 Null
1385        assert_eq!(model.get_data("contract_price"), Some(&Value::Null));
1386
1387        // 访问器返回 0.0(覆盖原始 Null)
1388        assert_eq!(model.get_attr("contract_price"), json!(0.0));
1389    }
1390
1391    #[test]
1392    fn test_php_consistency_set_attrs_preserves_batch_context() {
1393        // PHP 行为:setAttrs 中每个字段都能感知完整批量数据
1394        // 来源:Attribute.php 第 351-357 行
1395        let mut model = AccessorTestModel::new();
1396
1397        let mut batch = HashMap::new();
1398        batch.insert("field_a".to_string(), json!(50));
1399        batch.insert("field_b".to_string(), json!(99));
1400
1401        model.set_attrs(&batch);
1402
1403        // field_b 修改器读到 merged_data 中 field_a=50
1404        assert_eq!(model.data.get("field_b"), Some(&json!("50_99")));
1405        // field_a 原样写入(无修改器)
1406        assert_eq!(model.data.get("field_a"), Some(&json!(50)));
1407    }
1408
1409    // ====================================================================
1410    // Append 字段系统测试
1411    // ====================================================================
1412
1413    /// 测试用模型:实现完整 BaseModel + Accessor + Appendable
1414    ///
1415    /// 持有 `data: HashMap` + `get_cache: HashMap` + `append_state: AppendState`
1416    /// 模拟 PHP `$this->data` + `$this->get` + `$this->append` 三大实例状态。
1417    struct AppendableTestModel {
1418        data: HashMap<String, Value>,
1419        get_cache: HashMap<String, Value>,
1420        append_state: AppendState,
1421    }
1422
1423    impl AppendableTestModel {
1424        fn new() -> Self {
1425            Self {
1426                data: HashMap::new(),
1427                get_cache: HashMap::new(),
1428                append_state: AppendState::new(),
1429            }
1430        }
1431
1432        fn with_data(mut self, key: &str, value: Value) -> Self {
1433            self.data.insert(key.to_string(), value);
1434            self
1435        }
1436    }
1437
1438    impl Model for AppendableTestModel {
1439        type PrimaryKey = i64;
1440
1441        fn table_name() -> &'static str {
1442            "test_appendable"
1443        }
1444
1445        fn pk_name() -> &'static str {
1446            "id"
1447        }
1448
1449        fn pk(&self) -> Self::PrimaryKey {
1450            self.data.get("id").and_then(|v| v.as_i64()).unwrap_or(0)
1451        }
1452
1453        fn set_pk(&mut self, pk: Self::PrimaryKey) {
1454            self.data.insert("id".to_string(), json!(pk));
1455        }
1456    }
1457
1458    impl ModelExt for AppendableTestModel {
1459        fn columns() -> Vec<&'static str> {
1460            vec![
1461                "id",
1462                "status",
1463                "name",
1464                "password",
1465                "add_time",
1466                "sales_initial",
1467                "sales_actual",
1468            ]
1469        }
1470
1471        fn fillable() -> Vec<&'static str> {
1472            vec![
1473                "status",
1474                "name",
1475                "password",
1476                "add_time",
1477                "sales_initial",
1478                "sales_actual",
1479            ]
1480        }
1481
1482        fn guarded() -> Vec<&'static str> {
1483            vec!["id"]
1484        }
1485
1486        fn hidden() -> Vec<&'static str> {
1487            // password 在 hidden 中(对齐 PHP $hidden)
1488            vec!["password"]
1489        }
1490
1491        fn get_column_value(&self, column: &str) -> Option<OrmValue> {
1492            match column {
1493                "id" => self
1494                    .data
1495                    .get("id")
1496                    .and_then(|v| v.as_i64())
1497                    .map(OrmValue::I64),
1498                "status" => self
1499                    .data
1500                    .get("status")
1501                    .and_then(|v| v.as_i64())
1502                    .map(|i| OrmValue::I32(i as i32)),
1503                "name" => self
1504                    .data
1505                    .get("name")
1506                    .and_then(|v| v.as_str())
1507                    .map(|s| OrmValue::String(s.to_string())),
1508                "password" => self
1509                    .data
1510                    .get("password")
1511                    .and_then(|v| v.as_str())
1512                    .map(|s| OrmValue::String(s.to_string())),
1513                "add_time" => self
1514                    .data
1515                    .get("add_time")
1516                    .and_then(|v| v.as_i64())
1517                    .map(OrmValue::I64),
1518                "sales_initial" => self
1519                    .data
1520                    .get("sales_initial")
1521                    .and_then(|v| v.as_i64())
1522                    .map(OrmValue::I64),
1523                "sales_actual" => self
1524                    .data
1525                    .get("sales_actual")
1526                    .and_then(|v| v.as_i64())
1527                    .map(OrmValue::I64),
1528                _ => None,
1529            }
1530        }
1531
1532        fn from_value(&mut self, _map: HashMap<String, OrmValue>) {}
1533    }
1534
1535    impl RelationLoader for AppendableTestModel {
1536        fn get_relation(&self, _name: &str) -> Option<&OrmValue> {
1537            None
1538        }
1539        fn set_relation_data(&mut self, _name: &str, _data: OrmValue) {}
1540        fn get_relation_fk_value(&self, _fk_name: &str) -> String {
1541            String::new()
1542        }
1543    }
1544
1545    impl BaseModel for AppendableTestModel {
1546        fn append() -> Vec<&'static str> {
1547            // 静态 append:status_text(有访问器)+ no_accessor_field(无访问器)
1548            vec!["status_text", "no_accessor_field"]
1549        }
1550        // get_appended_value 默认返回 None
1551        // BaseModel::to_json_with_append 修正后:None → Value::Null
1552    }
1553
1554    impl Accessor for AppendableTestModel {
1555        fn data_map(&self) -> &HashMap<String, Value> {
1556            &self.data
1557        }
1558
1559        fn data_map_mut(&mut self) -> &mut HashMap<String, Value> {
1560            &mut self.data
1561        }
1562
1563        fn accessor_cache(&self) -> &HashMap<String, Value> {
1564            &self.get_cache
1565        }
1566
1567        fn accessor_cache_mut(&mut self) -> &mut HashMap<String, Value> {
1568            &mut self.get_cache
1569        }
1570
1571        /// 模拟 PHP 访问器派发:
1572        /// - status_text:基于 status 字段返回中文文案(GradeOrder/Order 模式)
1573        /// - stat_day:基于 add_time 时间戳格式化(ArtSave/UploadFile 模式)
1574        /// - product_sales:sales_initial + sales_actual 求和(Product 模式)
1575        fn accessor_for(&self, field: &str, _value: Option<&Value>) -> Value {
1576            match field {
1577                "status_text" => {
1578                    let status = self
1579                        .data
1580                        .get("status")
1581                        .and_then(|v| v.as_i64())
1582                        .unwrap_or(-1);
1583                    json!(match status {
1584                        0 => "禁用",
1585                        1 => "启用",
1586                        _ => "未知",
1587                    })
1588                }
1589                "stat_day" => {
1590                    // PHP getStatDayAttr($value, $data):基于 $data['add_time'] 格式化
1591                    let timestamp = self
1592                        .data
1593                        .get("add_time")
1594                        .and_then(|v| v.as_i64())
1595                        .unwrap_or(0);
1596                    json!(format!("day_{}", timestamp / 86400))
1597                }
1598                "product_sales" => {
1599                    // PHP getProductSalesAttr:sales_initial + sales_actual
1600                    let initial = self
1601                        .data
1602                        .get("sales_initial")
1603                        .and_then(|v| v.as_i64())
1604                        .unwrap_or(0);
1605                    let actual = self
1606                        .data
1607                        .get("sales_actual")
1608                        .and_then(|v| v.as_i64())
1609                        .unwrap_or(0);
1610                    json!(initial + actual)
1611                }
1612                _ => Value::Null,
1613            }
1614        }
1615    }
1616
1617    impl Appendable for AppendableTestModel {
1618        fn append_state(&self) -> &AppendState {
1619            &self.append_state
1620        }
1621
1622        fn append_state_mut(&mut self) -> &mut AppendState {
1623            &mut self.append_state
1624        }
1625    }
1626
1627    // -------------------- Append 基本功能测试 --------------------
1628
1629    #[test]
1630    fn test_base_model_to_json_with_append_outputs_null_for_no_accessor() {
1631        // PHP 行为:append 字段无访问器时仍输出,值为 null
1632        // 对齐 Conversion.php 第 292 行 $item[$name] = $this->getAttr($name)
1633        let model = AppendableTestModel::new()
1634            .with_data("id", json!(1))
1635            .with_data("status", json!(1));
1636        let json = model.to_json_with_append();
1637        // AppendableTestModel 未重写 get_appended_value,默认返回 None
1638        // 修正后:None → Value::Null
1639        assert_eq!(
1640            json["status_text"],
1641            Value::Null,
1642            "无访问器 append 字段应输出 null"
1643        );
1644        assert_eq!(
1645            json["no_accessor_field"],
1646            Value::Null,
1647            "无访问器 append 字段应输出 null"
1648        );
1649    }
1650
1651    #[test]
1652    fn test_appendable_to_json_with_append_cached_uses_accessor() {
1653        // PHP 行为:append 字段走 getAttr → 访问器
1654        let mut model = AppendableTestModel::new()
1655            .with_data("id", json!(1))
1656            .with_data("status", json!(1));
1657        let json = model.to_json_with_append_cached();
1658        // status_text 走 accessor_for → "启用"
1659        assert_eq!(json["status_text"], "启用");
1660        // no_accessor_field 走 accessor_for 默认分支 → Value::Null
1661        assert_eq!(json["no_accessor_field"], Value::Null);
1662    }
1663
1664    #[test]
1665    fn test_appendable_caches_accessor_result() {
1666        // PHP 行为:getAttr 缓存结果,多次调用只执行一次访问器
1667        // 同名字段修改不失效缓存(PHP bug 复刻)
1668        let mut model = AppendableTestModel::new()
1669            .with_data("id", json!(1))
1670            .with_data("status", json!(1));
1671        let json1 = model.to_json_with_append_cached();
1672        assert_eq!(json1["status_text"], "启用");
1673
1674        // 修改 status 不失效 status_text 缓存(PHP bug 复刻)
1675        model.data.insert("status".to_string(), json!(0));
1676        let json2 = model.to_json_with_append_cached();
1677        assert_eq!(
1678            json2["status_text"], "启用",
1679            "缓存命中,访问器不重新执行(PHP bug 复刻)"
1680        );
1681    }
1682
1683    #[test]
1684    fn test_append_field_bypasses_hidden_filter() {
1685        // PHP bug 复刻:append 字段绕过 hidden 过滤
1686        // 对齐 Conversion.php 第 291-296 行 appendAttrToArray 直接赋值
1687        let mut model = AppendableTestModel::new()
1688            .with_data("id", json!(1))
1689            .with_data("status", json!(1))
1690            .with_data("password", json!("secret"));
1691        let json = model.to_json_with_append_cached();
1692        // password 在 hidden 中,被过滤
1693        assert!(json.get("password").is_none(), "password 应被 hidden 过滤");
1694        // status_text 是 append 字段,绕过 hidden
1695        assert_eq!(json["status_text"], "启用");
1696    }
1697
1698    #[test]
1699    fn test_append_dyn_overrides_static_append() {
1700        // PHP 行为:$model->append($fields) 默认覆盖静态 $append
1701        // 对齐 Conversion.php 第 90-94 行
1702        let mut model = AppendableTestModel::new()
1703            .with_data("id", json!(1))
1704            .with_data("status", json!(1));
1705        // 静态 append: ["status_text", "no_accessor_field"]
1706        // 动态覆盖为 ["dynamic_field"]
1707        model.append_dyn(vec!["dynamic_field".to_string()]);
1708        let json = model.to_json_with_append_cached();
1709        // status_text 不再输出(被覆盖)
1710        assert!(
1711            json.get("status_text").is_none(),
1712            "status_text 应被动态 append 覆盖"
1713        );
1714        // no_accessor_field 不再输出(被覆盖)
1715        assert!(
1716            json.get("no_accessor_field").is_none(),
1717            "no_accessor_field 应被动态 append 覆盖"
1718        );
1719        // dynamic_field 输出(走 accessor_for 默认分支 → null)
1720        assert_eq!(json["dynamic_field"], Value::Null);
1721    }
1722
1723    #[test]
1724    fn test_append_merge_combines_with_static() {
1725        // PHP 行为:$model->append($fields, true) 合并到静态 $append
1726        // 对齐 Conversion.php 第 91-93 行
1727        let mut model = AppendableTestModel::new()
1728            .with_data("id", json!(1))
1729            .with_data("status", json!(1));
1730        model.append_merge(vec!["extra_field".to_string()]);
1731        let json = model.to_json_with_append_cached();
1732        // 静态字段保留
1733        assert_eq!(json["status_text"], "启用");
1734        assert_eq!(json["no_accessor_field"], Value::Null);
1735        // 合并的字段输出
1736        assert_eq!(json["extra_field"], Value::Null);
1737    }
1738
1739    #[test]
1740    fn test_append_dyn_returns_self_for_chaining() {
1741        // PHP 行为:append() 返回 $this,支持链式
1742        // 对齐 Conversion.php 第 96 行
1743        let mut model = AppendableTestModel::new()
1744            .with_data("id", json!(1))
1745            .with_data("status", json!(1));
1746        // 链式调用
1747        model
1748            .append_merge(vec!["field1".to_string()])
1749            .append_merge(vec!["field2".to_string()]);
1750        let json = model.to_json_with_append_cached();
1751        assert!(json.get("field1").is_some(), "链式 append_merge 应生效");
1752        assert!(json.get("field2").is_some(), "链式 append_merge 应生效");
1753        // 静态字段也保留
1754        assert_eq!(json["status_text"], "启用");
1755    }
1756
1757    #[test]
1758    fn test_effective_append_priority() {
1759        // 动态 append 优先于静态
1760        let model = AppendableTestModel::new();
1761        // 默认使用静态
1762        assert_eq!(
1763            model.effective_append(),
1764            vec!["status_text".to_string(), "no_accessor_field".to_string()]
1765        );
1766
1767        let mut model = model;
1768        model.append_dyn(vec!["override".to_string()]);
1769        assert_eq!(model.effective_append(), vec!["override".to_string()]);
1770    }
1771
1772    #[test]
1773    fn test_append_state_replace_and_merge() {
1774        // 直接测试 AppendState 行为
1775        let mut state = AppendState::new();
1776        assert!(state.dynamic_fields().is_none(), "初始状态无动态字段");
1777
1778        state.replace(vec!["a".to_string(), "b".to_string()]);
1779        assert_eq!(
1780            state.dynamic_fields().unwrap(),
1781            &vec!["a".to_string(), "b".to_string()]
1782        );
1783
1784        // merge 去重
1785        state.merge(vec!["b".to_string(), "c".to_string()]);
1786        assert_eq!(
1787            state.dynamic_fields().unwrap(),
1788            &vec!["a".to_string(), "b".to_string(), "c".to_string()]
1789        );
1790    }
1791
1792    // -------------------- Append PHP 一致性测试(R5 硬约束)--------------------
1793
1794    #[test]
1795    fn test_php_consistency_status_text_pattern() {
1796        // PHP 模式:*_text 后缀访问器,基于状态码返回中文文案
1797        // 代表模型:GradeOrder, Order
1798        let test_cases = vec![(0i64, "禁用"), (1, "启用"), (99, "未知")];
1799        for (status, expected) in test_cases {
1800            let mut model = AppendableTestModel::new()
1801                .with_data("id", json!(1))
1802                .with_data("status", json!(status));
1803            let json = model.to_json_with_append_cached();
1804            assert_eq!(
1805                json["status_text"], expected,
1806                "status={} 应返回 '{}'",
1807                status, expected
1808            );
1809        }
1810    }
1811
1812    #[test]
1813    fn test_php_consistency_stat_day_pattern() {
1814        // PHP 模式:时间戳格式化为日期
1815        // 代表模型:ArtSave, UploadFile
1816        // PHP getStatDayAttr($value, $data):$value 为 stat_day 字段值(不存在→null),
1817        // $data['add_time'] 为时间戳,格式化为 "Y-m-d"
1818        let mut model = AppendableTestModel::new()
1819            .with_data("id", json!(1))
1820            .with_data("add_time", json!(1690000000));
1821        // 动态追加 stat_day(验证动态 append + 访问器派发)
1822        model.append_merge(vec!["stat_day".to_string()]);
1823        let json = model.to_json_with_append_cached();
1824        // 1690000000 / 86400 = 19560(天)
1825        assert_eq!(json["stat_day"], "day_19560");
1826    }
1827
1828    #[test]
1829    fn test_php_consistency_product_sales_pattern() {
1830        // PHP 模式:计算字段(多字段求和)
1831        // 代表模型:Product::getProductSalesAttr = sales_initial + sales_actual
1832        let mut model = AppendableTestModel::new()
1833            .with_data("id", json!(1))
1834            .with_data("sales_initial", json!(100))
1835            .with_data("sales_actual", json!(50));
1836        // 动态追加 product_sales
1837        model.append_merge(vec!["product_sales".to_string()]);
1838        let json = model.to_json_with_append_cached();
1839        assert_eq!(json["product_sales"], 150);
1840    }
1841
1842    #[test]
1843    fn test_php_consistency_append_always_outputs_even_without_accessor() {
1844        // PHP 行为:append 字段无访问器时,toArray 仍输出该字段,值为 null
1845        // 对齐 Conversion.php 第 292 行 $item[$name] = $this->getAttr($name)
1846        // getAttr 无访问器且字段不在 $data 中 → 返回 null
1847        let mut model = AppendableTestModel::new()
1848            .with_data("id", json!(1))
1849            .with_data("status", json!(1));
1850        // 静态 append 包含 no_accessor_field(无访问器)
1851        let json = model.to_json_with_append_cached();
1852        // no_accessor_field 走 accessor_for 默认分支 → Value::Null
1853        assert_eq!(
1854            json["no_accessor_field"],
1855            Value::Null,
1856            "PHP 行为复刻:append 字段无访问器应输出 null"
1857        );
1858    }
1859
1860    #[test]
1861    fn test_php_consistency_append_overrides_hidden() {
1862        // PHP bug 复刻:append 字段绕过 hidden 过滤
1863        // 对齐 Conversion.php 第 291-296 行
1864        // 即使 append 字段名在 hidden 列表中,仍会输出
1865        let mut model = AppendableTestModel::new()
1866            .with_data("id", json!(1))
1867            .with_data("status", json!(1))
1868            .with_data("password", json!("secret"));
1869        // password 在 hidden 中
1870        // status_text 是 append 字段,绕过 hidden
1871        let json = model.to_json_with_append_cached();
1872        assert!(json.get("password").is_none(), "password 应被 hidden 过滤");
1873        assert_eq!(json["status_text"], "启用", "append 字段应绕过 hidden");
1874    }
1875
1876    #[test]
1877    fn test_php_consistency_dynamic_append_overrides_static() {
1878        // PHP 行为:$model->append($fields) 默认覆盖静态 $append
1879        // 对齐 Conversion.php 第 90-94 行
1880        let mut model = AppendableTestModel::new()
1881            .with_data("id", json!(1))
1882            .with_data("status", json!(1));
1883        // 动态覆盖
1884        model.append_dyn(vec!["stat_day".to_string()]);
1885        let json = model.to_json_with_append_cached();
1886        // 静态 append 字段不再输出
1887        assert!(
1888            json.get("status_text").is_none(),
1889            "动态 append 应覆盖静态,status_text 不应输出"
1890        );
1891        // 动态字段输出
1892        assert!(json.get("stat_day").is_some(), "动态 append 字段应输出");
1893    }
1894
1895    #[test]
1896    fn test_php_consistency_append_method_returns_this_for_chaining() {
1897        // PHP 行为:append() 返回 $this,支持链式调用
1898        // 对齐 Conversion.php 第 96 行 return $this
1899        let mut model = AppendableTestModel::new()
1900            .with_data("id", json!(1))
1901            .with_data("status", json!(1));
1902        // 链式调用:append_merge → append_merge
1903        model
1904            .append_merge(vec!["stat_day".to_string()])
1905            .append_merge(vec!["product_sales".to_string()]);
1906        let json = model.to_json_with_append_cached();
1907        // 三个 append 字段都应输出(静态 2 个 + 动态合并 2 个)
1908        assert_eq!(json["status_text"], "启用");
1909        assert!(json.get("stat_day").is_some());
1910        assert!(json.get("product_sales").is_some());
1911    }
1912}