Bladeren bron

feat(core): 添加 EAV 模型扩展

- 新增 EAV 模型相关的数据库迁移文件- 创建 EAV 模型的实体类和控制器
- 实现 EAV 模型的核心功能,包括实体类型、属性、值等
- 添加 EAV 模型的路由和扩展信息
-编写 EAV 模型的使用说明文档
runphp 7 maanden geleden
commit
dbca0df1cb

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+.idea/

+ 12 - 0
README.md

@@ -0,0 +1,12 @@
+# EAV(Entity-Attribute-Value)模型
+
+用于开发不同项目有差异的系统,如:用户、产品、订单等。
+
+## EAV模型核心组件
+
+- 实体(Entity)是一个具有唯一标识符的对象,它可以表示任何类型的数据,例如用户、产品、订单等。
+- 属性(Attribute)是实体的特征或属性,它定义了实体的结构和数据类型。属性可以是简单的文本、数字、日期等。
+- 值(Value)是属性的值,它保存了实体的属性值。例如,一个用户实体的属性可能包括名称、邮箱地址、手机号码等,而值则对应于这些属性的值。
+
+
+

+ 31 - 0
composer.json

@@ -0,0 +1,31 @@
+{
+  "name": "six-shop/eav",
+  "description": "EAV(Entity-Attribute-Value)模型",
+  "type": "sixshop-extension",
+  "keywords": [
+    "sixshop",
+    "thinkphp"
+  ],
+  "require": {
+    "php": ">=8.3",
+    "six-shop/core": ">=0.4 <1.0"
+  },
+  "authors": [
+    {
+      "name": "hui he",
+      "email": "runphp@qq.com"
+    }
+  ],
+  "license": "MIT",
+  "autoload": {
+    "psr-4": {
+      "SixShop\\eav\\": "src"
+    }
+  },
+  "extra": {
+    "sixshop": {
+      "id": "hello",
+      "class": "SixShop\\eav\\Extension"
+    }
+  }
+}

+ 38 - 0
database/migrations/20250712021908_extension_eva_entity_type.php

@@ -0,0 +1,38 @@
+<?php
+
+use think\migration\Migrator;
+
+class ExtensionEvaEntityType extends Migrator
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
+     *
+     * The following commands can be used in this method and Phinx will
+     * automatically reverse them when rolling back:
+     *
+     *    createTable
+     *    renameTable
+     *    addColumn
+     *    renameColumn
+     *    addIndex
+     *    addForeignKey
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change(): void
+    {
+        $table = $this->table('extension_eva_entity_type', ['engine' => 'InnoDB', 'comment' => '实体类型表(如商品/用户等)']);
+        $table->addColumn('entity_type_code', 'string', ['limit' => 50, 'comment' => '实体类型编码'])
+            ->addColumn('entity_table', 'string', ['limit' => 64, 'comment' => '实体主表名'])
+            ->addIndex(['entity_type_code'], ['name' => 'entity_type_code', 'unique' => true])
+            ->addTimestamps()
+            ->addSoftDelete()
+            ->create();
+    }
+}

+ 43 - 0
database/migrations/20250712021931_extension_eva_attribute.php

@@ -0,0 +1,43 @@
+<?php
+
+use think\migration\Migrator;
+
+class ExtensionEvaAttribute extends Migrator
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
+     *
+     * The following commands can be used in this method and Phinx will
+     * automatically reverse them when rolling back:
+     *
+     *    createTable
+     *    renameTable
+     *    addColumn
+     *    renameColumn
+     *    addIndex
+     *    addForeignKey
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change(): void
+    {
+        $this->table('extension_eva_attribute', ['engine' => 'InnoDB', 'charset' => 'utf8mb4', 'comment' => '属性定义表'])
+            ->addColumn('entity_type_id', 'integer', ['comment' => '关联实体类型'])
+            ->addColumn('attribute_code', 'string', ['comment' => '属性编码', 'limit' => 64])
+            ->addColumn('backend_type', 'string', ['comment' => '存储类型(int/varchar/decimal/text), 需与frontend_input匹配', 'limit' => 8])
+            ->addColumn('frontend_input', 'string', ['comment' => '表单控件类型', 'limit' => 16])
+            ->addColumn('frontend_label', 'string', ['comment' => '显示标签', 'limit' => 255])
+            ->addColumn('is_required', 'boolean', ['comment' => '是否必填', 'default' => false])
+            ->addIndex(['entity_type_id', 'attribute_code'], ['unique' => true])
+            ->addTimestamps()
+            ->addSoftDelete()
+            ->create();
+
+    }
+}

+ 45 - 0
database/migrations/20250712022607_extension_eva_value.php

@@ -0,0 +1,45 @@
+<?php
+
+use think\migration\Migrator;
+use think\migration\db\Column;
+
+class ExtensionEvaValue extends Migrator
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
+     *
+     * The following commands can be used in this method and Phinx will
+     * automatically reverse them when rolling back:
+     *
+     *    createTable
+     *    renameTable
+     *    addColumn
+     *    renameColumn
+     *    addIndex
+     *    addForeignKey
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change(): void
+    {
+        $table = $this->table('extension_eva_value', ['engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '扩展:EAV值表']);
+        $table
+            ->addColumn('entity_id', 'integer', ['limit' => 11, 'default' => 0, 'comment' => '实体ID'])
+            ->addColumn('attribute_id', 'integer', ['limit' => 11, 'default' => 0, 'comment' => '属性ID'])
+            ->addColumn('value_int', 'integer', ['limit' => 11, 'default' => 0, 'comment' => '整数值'])
+            ->addColumn('value_varchar', 'string', ['limit' => 255, 'default' => '', 'comment' => '字符串值'])
+            ->addColumn('value_decimal', 'decimal', ['precision' => 10, 'scale' => 2, 'default' => 0.00, 'comment' => '数值值'])
+            ->addColumn('value_text', 'text', ['comment' => '文本值'])
+            ->addColumn('value_datetime', 'datetime', ['comment' => '日期时间值'])
+            ->addIndex(['entity_id', 'attribute_id'], ['name' => 'entity_id_attribute_id', 'unique' => true])
+            ->addTimestamps()
+            ->addSoftDelete()
+            ->create();
+    }
+}

+ 41 - 0
database/migrations/20250712025652_extension_eva_attribute_option.php

@@ -0,0 +1,41 @@
+<?php
+
+use think\migration\Migrator;
+use think\migration\db\Column;
+
+class ExtensionEvaAttributeOption extends Migrator
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
+     *
+     * The following commands can be used in this method and Phinx will
+     * automatically reverse them when rolling back:
+     *
+     *    createTable
+     *    renameTable
+     *    addColumn
+     *    renameColumn
+     *    addIndex
+     *    addForeignKey
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change()
+    {
+        $table = $this->table('extension_eva_attribute_option', ['engine' => 'InnoDB', 'comment' => '属性选项表']);
+        $table->addColumn(Column::integer('attribute_id')->setComment('属性ID'))
+            ->addColumn(Column::string('label')->setComment('显示文本'))
+            ->addColumn(Column::string('value')->setComment('选项值'))
+            ->addColumn(Column::integer('sort')->setComment('排序'))
+            ->addTimestamps()
+            ->addSoftDelete()
+            ->addIndex(['attribute_id'])
+            ->create();
+    }
+}

+ 17 - 0
info.php

@@ -0,0 +1,17 @@
+<?php
+declare(strict_types=1);
+
+return [
+    'id' => 'eav', # 扩展的唯一标识符
+    'name' => 'EAV模型', # 扩展的名称
+    'is_core' => true, # 是否核心扩展'
+    'category' => 'core', # 扩展的分类 core:核心扩展,other:其他扩展
+    'description' => 'EAV模型扩展。', # 扩展的描述
+    'version' => '1.0.0',  # 扩展的版本
+    'core_version' => '^1.0',  # 支持的核心版本
+    'author' => 'runphp', # 作者
+    'email' => 'runphp@qq.com', # 作者的邮箱
+    'website' => '', # 扩展的地址,可以是扩展的仓库地址,帮助用户寻找扩展,安装扩展等网络地址
+    'image' => '', # 扩展的图片,用于展示扩展的图标,或者是扩展的截图等图片地址
+    'license' => 'MIT', # 扩展的开源协议
+];

+ 21 - 0
route/admin.php

@@ -0,0 +1,21 @@
+<?php
+declare(strict_types=1);
+
+use SixShop\Extension\eav\Controller\{AttributeOptionController,
+    EntityAttributeController,
+    EntityTypeController,
+    ValueController};
+use think\facade\Route;
+
+Route::resource('entity_type', EntityTypeController::class)->middleware([
+    'auth'
+]);
+Route::resource('entity_attribute', EntityAttributeController::class)->middleware([
+    'auth'
+]);
+Route::resource('attribute_option', AttributeOptionController::class)->middleware([
+    'auth'
+]);
+Route::resource('value', ValueController::class)->middleware([
+    'auth'
+]);

+ 32 - 0
src/Controller/AttributeOptionController.php

@@ -0,0 +1,32 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\eav\Controller;
+
+use SixShop\Core\Helper;
+use SixShop\Core\Request;
+use SixShop\eav\Entity\EvaAttributeOptionEntity;
+use think\Response;
+use think\response\Json;
+
+class AttributeOptionController
+{
+    public function index(Request $request, EvaAttributeOptionEntity $entity): Response
+    {
+        $attributeId = $request->get('attribute_id/d');
+        return Helper::success_response($entity->where(['attribute_id' =>$attributeId])->select());
+    }
+
+    public function save(Request $request, EvaAttributeOptionEntity $entity): Response
+    {
+        $data = $request->post();
+        return Helper::success_response($entity->save($data));
+    }
+    public function update(int $id, Request $request, EvaAttributeOptionEntity $entity): Response
+    {
+        return Helper::success_response($entity->update($request->post(), ['id' => $id]));
+    }
+    public function delete(int $id, EvaAttributeOptionEntity $entity): Response
+    {
+        return Helper::success_response($entity->destroy($id));
+    }
+}

+ 42 - 0
src/Controller/EntityAttributeController.php

@@ -0,0 +1,42 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\eav\Controller;
+
+use SixShop\Core\Helper;
+use SixShop\Core\Request;
+use SixShop\eav\Entity\EvaAttributeEntity;
+use think\Response;
+
+class EntityAttributeController
+{
+    public function index(Request $request, EvaAttributeEntity $entity): Response
+    {
+        $entityTypeID = $request->get('entity_type_id/d');
+        return Helper::success_response($entity->where('entity_type_id', $entityTypeID)->select());
+    }
+
+    public function save(Request $request, EvaAttributeEntity $entity): Response
+    {
+        $data = $request->post();
+        $entityObject = $entity->withTrashed()->where([
+            'entity_type_id' => $data['entity_type_id'],
+            'attribute_code' => $data['attribute_code'
+            ]])->findOrEmpty();
+        if (!$entityObject->isEmpty()) {
+            $entityObject->restore();
+            $data['id'] = $entityObject['id'];
+        }
+        return Helper::success_response($entityObject->save($data));
+    }
+
+    public function update(int $id, Request $request, EvaAttributeEntity $entity): Response
+    {
+        $data = $request->post();
+        return Helper::success_response($entity->where('id', $id)->update($data));
+    }
+
+    public function delete(int $id, EvaAttributeEntity $entity): Response
+    {
+        return Helper::success_response($entity->destroy($id));
+    }
+}

+ 42 - 0
src/Controller/EntityTypeController.php

@@ -0,0 +1,42 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\eav\Controller;
+
+use SixShop\Core\Helper;
+use SixShop\Core\Request;
+use SixShop\eav\Entity\EvaEntityTypeEntity;
+use think\Response;
+
+class EntityTypeController
+{
+    public function index(EvaEntityTypeEntity $entityTypeEntity): Response
+    {
+        return Helper::success_response($entityTypeEntity->select());
+    }
+
+    public function read(int $id, EvaEntityTypeEntity $entity): Response
+    {
+        return Helper::success_response($entity->find($id));
+    }
+    public function save(Request $request, EvaEntityTypeEntity $entity): Response
+    {
+        $data = $request->post();
+        $entityObject = $entity->withTrashed()
+            ->where('entity_type_code', $data['entity_type_code'])
+            ->findOrEmpty();
+        if (!$entityObject->isEmpty()) {
+            $entityObject->restore();
+            $data['id'] = $entityObject['id'];
+        }
+        return Helper::success_response($entityObject->save($data));
+    }
+
+    public function update(int $id, Request $request, EvaEntityTypeEntity $entity): Response
+    {
+        return Helper::success_response($entity->update($request->post(), ['id' => $id]));
+    }
+    public function delete(int $id, EvaEntityTypeEntity $entity): Response
+    {
+        return Helper::success_response($entity->destroy($id));
+    }
+}

+ 104 - 0
src/Controller/ValueController.php

@@ -0,0 +1,104 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\eav\Controller;
+
+use SixShop\Core\Helper;
+use SixShop\Core\Request;
+use SixShop\eav\Entity\EvaValueEntity;
+use SixShop\eav\Model\EvaAttributeModel;
+use think\Response;
+
+class ValueController
+{
+    /**
+     * 查询指定实体的所有属性值
+     * GET /eav/value?entity_type_id=1&entity_id=123
+     */
+    public function index(Request $request, EvaValueEntity $entity): Response
+    {
+        $entityTypeId = $request->get('entity_type_id/d');
+        $entityId = $request->get('entity_id/d');
+        if (!$entityTypeId || !$entityId) {
+            return Helper::error_response('参数缺失');
+        }
+        // 查询该实体的所有属性值
+        $list = $entity->where([
+            'entity_id' => $entityId
+        ])->select();
+        return Helper::success_response($list);
+    }
+
+    /**
+     * 保存/更新指定实体的属性值
+     * POST /eav/value
+     * body: [
+     *   { "entity_id": 123, "entity_type_id": 1, "attribute_id": 2, "value": "红色" }
+     * ]
+     */
+    public function save(Request $request, EvaValueEntity $entity): Response
+    {
+        $data = $request->post();
+        if (empty($data) || !is_array($data)) {
+            return Helper::error_response('参数错误');
+        }
+        // 支持批量写入
+        foreach ($data as $item) {
+            if (
+                empty($item['entity_id']) ||
+                empty($item['attribute_id']) ||
+                !isset($item['value'])
+            ) {
+                continue;
+            }
+            // 获取属性类型
+            $attr = EvaAttributeModel::find($item['attribute_id']);
+            if (!$attr) {
+                continue;
+            }
+            $saveData = [
+                'entity_id'    => $item['entity_id'],
+                'attribute_id' => $item['attribute_id'],
+            ];
+            switch ($attr->backend_type) {
+                case 'int':
+                    $saveData['value_int'] = intval($item['value']);
+                    break;
+                case 'decimal':
+                    $saveData['value_decimal'] = floatval($item['value']);
+                    break;
+                case 'text':
+                    $saveData['value_text'] = $item['value'];
+                    break;
+                case 'varchar':
+                default:
+                    $saveData['value_varchar'] = $item['value'];
+            }
+            // 先查找是否已存在,存在则更新,否则新增
+            $exist = $entity->where([
+                'entity_id' => $item['entity_id'],
+                'attribute_id' => $item['attribute_id']
+            ])->find();
+            if ($exist) {
+                $entity->where('id', $exist['id'])->update($saveData);
+            } else {
+                $entity->save($saveData);
+            }
+        }
+        return Helper::success_response('保存成功');
+    }
+
+    /**
+     * 删除指定实体的所有属性值
+     * DELETE /eav/value?entity_id=123
+     */
+    public function delete(Request $request, EvaValueEntity $entity): Response
+    {
+        $entityId = $request->get('entity_id/d');
+        if (!$entityId) {
+            return Helper::error_response('参数缺失');
+        }
+        $entity->where(['entity_id' => $entityId])->delete();
+        return Helper::success_response('删除成功');
+    }
+}

+ 10 - 0
src/Entity/EvaAttributeEntity.php

@@ -0,0 +1,10 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\eav\Entity;
+
+use SixShop\Core\Entity\BaseEntity;
+
+class EvaAttributeEntity extends BaseEntity
+{
+
+}

+ 10 - 0
src/Entity/EvaAttributeOptionEntity.php

@@ -0,0 +1,10 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\eav\Entity;
+
+use SixShop\Core\Entity\BaseEntity;
+
+class EvaAttributeOptionEntity extends BaseEntity
+{
+
+}

+ 16 - 0
src/Entity/EvaEntityTypeEntity.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace SixShop\eav\Entity;
+
+use SixShop\eav\Model\EvaEntityTypeModel;
+use think\Entity;
+
+class EvaEntityTypeEntity extends Entity
+{
+    protected function getOptions(): array
+    {
+        return [
+            'modelClass' => EvaEntityTypeModel::class,
+        ];
+    }
+}

+ 11 - 0
src/Entity/EvaValueEntity.php

@@ -0,0 +1,11 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\eav\Entity;
+
+use SixShop\Core\Entity\BaseEntity;
+
+class EvaValueEntity extends BaseEntity
+{
+
+}

+ 16 - 0
src/Extension.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\eav;
+
+
+use SixShop\Core\ExtensionAbstract;
+
+class Extension extends ExtensionAbstract
+{
+
+    protected function getBaseDir(): string
+    {
+        return dirname(__DIR__);
+    }
+}

+ 29 - 0
src/Model/EvaAttributeModel.php

@@ -0,0 +1,29 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\eav\Model;
+
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * Class SixShop\eav\Model\ExtensionEvaAttributeModel
+ *
+ * @property bool $is_required 是否必填
+ * @property int $entity_type_id 关联实体类型
+ * @property int $id
+ * @property string $attribute_code 属性编码
+ * @property string $backend_type 存储类型(int/varchar/decimal/text), 需与frontend_input匹配
+ * @property string $create_time
+ * @property string $delete_time
+ * @property string $frontend_input 表单控件类型
+ * @property string $frontend_label 显示标签
+ * @property string $update_time
+ * @method static \think\db\Query onlyTrashed()
+ * @method static \think\db\Query withTrashed()
+ */
+class EvaAttributeModel extends Model
+{
+    protected $name = 'extension_eva_attribute';
+    protected $pk = 'id';
+    use SoftDelete;
+}

+ 28 - 0
src/Model/EvaAttributeOptionModel.php

@@ -0,0 +1,28 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\eav\Model;
+
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * Class SixShop\eav\Model\ExtensionEvaAttributeOptionModel
+ *
+ * @property int $attribute_id 属性ID
+ * @property int $id
+ * @property int $sort 排序
+ * @property string $create_time
+ * @property string $delete_time
+ * @property string $label 显示文本
+ * @property string $update_time
+ * @property string $value 选项值
+ * @method static \think\db\Query onlyTrashed()
+ * @method static \think\db\Query withTrashed()
+ */
+class EvaAttributeOptionModel extends Model
+{
+    protected $name = 'extension_eva_attribute_option';
+    protected $pk = 'id';
+    use SoftDelete;
+}

+ 25 - 0
src/Model/EvaEntityTypeModel.php

@@ -0,0 +1,25 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\eav\Model;
+
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * Class SixShop\eav\Model\ExtensionEvaEntityTypeModel
+ *
+ * @property int $id
+ * @property string $create_time
+ * @property string $delete_time
+ * @property string $entity_table 实体主表名
+ * @property string $entity_type_code 实体类型编码
+ * @property string $update_time
+ * @method static \think\db\Query onlyTrashed()
+ * @method static \think\db\Query withTrashed()
+ */
+class EvaEntityTypeModel extends Model
+{
+    protected $name = 'extension_eva_entity_type';
+    protected $pk = 'id';
+    use SoftDelete;
+}

+ 30 - 0
src/Model/EvaValueModel.php

@@ -0,0 +1,30 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\eav\Model;
+
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * Class SixShop\eav\Model\ExtensionEvaValueModel
+ *
+ * @property float $value_decimal 数值值
+ * @property int $attribute_id 属性ID
+ * @property int $entity_id 实体ID
+ * @property int $id
+ * @property int $value_int 整数值
+ * @property string $create_time
+ * @property string $delete_time
+ * @property string $update_time
+ * @property string $value_datetime 日期时间值
+ * @property string $value_text 文本值
+ * @property string $value_varchar 字符串值
+ * @method static \think\db\Query onlyTrashed()
+ * @method static \think\db\Query withTrashed()
+ */
+class EvaValueModel extends Model
+{
+    protected $name = 'extension_eva_value';
+    protected $pk = 'id';
+    use SoftDelete;
+}