laravel5.7 电商系统商品与众筹功能数据库设计与数据关联关系及总结

在应用laravel5.7框架开发电商系统的过程中,涉及到产品信息的存储问题,当然不能将所有的产品信息存储在一张表中,还需要关于产品信息关联的辅助表,我们来分析一下关于商城的基本功能与数据分析。现在主流商城的基本模式有 B to B与 B to C 两种模式我们以 B to C 为例来分析数据与逻辑:

1.首先是进行数据库设计,我们需要那些数据表:

表名意义关联关系
products产品信息表,对应数据模型 Product
product_skus产品的 SKU 表,对应数据模型 ProductSku与products表关联
crowdfunding_products产品的 众筹 表,对应数据模型 CrowdfundingProduct与products表关联

2.数据表的字段设计

接下来我们需要整理好 products 表、 product_skus 表和crowdfunding_products表的字段名称和类型:

products 表:

字段名称描述类型加索引缘由
id自增长IDunsigned int主键
type商品类型varchar
title商品名称varchar
description商品详情text
image商品封面图片文件路径varchar
on_sale商品是否正在售卖tiny int, default 1
rating商品平均评分float, default 5
sold_count销量unsigned int, default 0
review_count评价数量unsigned int, default 0
priceSKU 最低价格decimal

商品本身是没有固定的价格的 price字段是为了方便用户进行搜索与排序的


product_skus 表:

字段名称描述类型加索引缘由
id自增长IDunsigned int主键
titleSKU 名称varchar
descriptionSKU 描述varchar
priceSKU 价格decimal
stock库存unsigne int
product_id所属商品 idunsigne int外键

crowdfunding_products 表:

字段名称描述类型加索引缘由
id自增长IDunsigned int主键
product_id对应商品表的 IDunsigned int外键
target_amount众筹目标金额decimal
total_amount当前已筹金额decimal
user_count当前参与众筹用户数unsigned int
end_at众筹结束时间datetime
status当前筹款的状态varchar

电商项目中与钱相关的有小数点的字段一律使用 decimal 类型,而不是 float double,后面两种类型在做小数运算时有可能出现精度丢失的问题,这在电商系统里是绝对不允许出现的。

3.创建相应的模型文件

接下来我们根据整理好的字段创建对应的模型文件:

$ php artisan make:model Models/Product -mf
$ php artisan make:model Models/ProductSku -mf
$ php artisan make:model Models/CrowdfundingProduct -m

database/migrations/数据表模型文件在这个目录下,根据上面整理好的字段修改数据库迁移文件。

database/migrations/< your_date >_create_products_table.php

    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
             $table->string('type')->after('id')->default(\App\Models\Product::TYPE_NORMAL)->index();
            $table->string('title');
            $table->text('description');
            $table->string('image');
            $table->boolean('on_sale')->default(true);
            $table->float('rating')->default(5);
            $table->unsignedInteger('sold_count')->default(0);
            $table->unsignedInteger('review_count')->default(0);
            $table->decimal('price', 10, 2);
            $table->timestamps();
        });
    }

\App\Models\Product::TYPE_NORMAL为引用产品模型中定义的普通商品
$table->string('type')->after('id')->default(\App\Models\Product::TYPE_NORMAL)->index();是定义type字段默认数据是normal,在接下来的模型文件代码中就能够看到这个设置

database/migrations/< your_date >_create_product_skus_table.php

    public function up()
    {
        Schema::create('product_skus', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->string('description');
            $table->decimal('price', 10, 2);
            $table->unsignedInteger('stock');
            $table->unsignedInteger('product_id');
            $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
            $table->timestamps();
        });
    }

database/migrations/< your_date >_create_crowdfunding_products_table.php

    public function up()
    {
        Schema::create('crowdfunding_products', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('product_id');
            $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
            $table->decimal('target_amount', 10, 2);
            $table->decimal('total_amount', 10, 2)->default(0);
            $table->unsignedInteger('user_count')->default(0);
            $table->dateTime('end_at');
            $table->string('status')->default(\App\Models\CrowdfundingProduct::STATUS_FUNDING);
        });
    }

\App\Models\CrowdfundingProduct::STATUS_FUNDING在模型中定义的常量请看下面代码


4.接下来我们来定义对应的模型文件:

4.1 app/Models/Product.php

    const TYPE_NORMAL = 'normal';
    const TYPE_CROWDFUNDING = 'crowdfunding';
    public static $typeMap = [
        self::TYPE_NORMAL  => '普通商品',
        self::TYPE_CROWDFUNDING => '众筹商品',
    ];
    protected $fillable = [
                    'title', 'description', 'image', 'on_sale', 
                    'rating', 'sold_count', 'review_count', 'price',
                    'type',
    ];
    protected $casts = [
        'on_sale' => 'boolean', // on_sale 是一个布尔类型的字段
    ];
    // 与商品SKU一对多关联
    public function skus()
    {
        return $this->hasMany(ProductSku::class);
    }
    //与众筹表一对一关联
    public function crowdfunding()
    {
        return $this->hasOne(CrowdfundingProduct::class);
    }

4.2 app/Models/ProductSku.php

    protected $fillable = ['title', 'description', 'price', 'stock'];
    //与商品表反向关联
    public function product()
    {
        return $this->belongsTo(Product::class);
    }

4.3 app/Models/CrowdfundingProduct.php

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class CrowdfundingProduct extends Model
{
    // 定义众筹的 3 种状态
    const STATUS_FUNDING = 'funding';
    const STATUS_SUCCESS = 'success';
    const STATUS_FAIL = 'fail';

    public static $statusMap = [
        self::STATUS_FUNDING => '众筹中',
        self::STATUS_SUCCESS => '众筹成功',
        self::STATUS_FAIL    => '众筹失败',
    ];

    protected $fillable = ['total_amount', 'target_amount', 'user_count', 'status', 'end_at'];
    // end_at 会自动转为 Carbon 类型
    protected $dates = ['end_at'];
    // 不需要 created_at 和 updated_at 字段
    public $timestamps = false;
    //与商品表关联
    public function product()
    {
        return $this->belongsTo(Product::class);
    }

    // 定义一个名为 percent 的访问器,返回当前众筹进度
    public function getPercentAttribute()
    {
        // 已筹金额除以目标金额
        $value = $this->attributes['total_amount'] / $this->attributes['target_amount'];

        return floatval(number_format($value * 100, 2, '.', ''));
    }
}

饭后执行数据迁移命令:

$ php artisan migrate

这样三个关于产品的数据表与模型就是创建完毕了,执行完后会在数据库管理器中发现三个表已经生成。


5.创建控制器

因为我用的是laravel-admin管理后台所以用 admin:make 来创建管理后台的控制器:

$ php artisan admin:make ProductsController --model=App\\Models\\Product
$ php artisan admin:make CrowdfundingProductsController --model=App\\Models\\Product

5.1商品SKU数据的交互是在控制器ProductsControlle中完成的关于产品列表部分也就是$grid()部分就不说了。

接下来着重说一下产品编辑中因为需要添加产品的SKU数据所以需要在$form()中做些改动代码如下

        // 直接添加一对多的关联模型
        $form->hasMany('skus', 'SKU 列表', function (Form\NestedForm $form) {
            $form->text('title', 'SKU 名称')->rules('required');
            $form->text('description', 'SKU 描述')->rules('required');
            $form->text('price', '单价')->rules('required|numeric|min:0.01');
            $form->text('stock', '剩余库存')->rules('required|integer|min:0');
        });
        // 定义事件回调,当模型即将保存时会触发这个回调
        $form->saving(function (Form $form) {
            $form->model()->price = collect($form->input('skus'))->where(Form::REMOVE_FLAG_NAME, 0)->min('price') ?: 0;
        });

代码解析:

  • $form->hasMany('skus', 'SKU 列表', /**/) 可以在表单中直接添加一对多的关联模型,商品和商品 SKU 的关系就是一对多,第一个参数必须和主模型中定义此关联关系的方法同名,我们之前在 App\Models\Product 类中定义了 skus() 方法来关联 SKU,因此这里我们需要填入 skus,第二个参数是对这个关联关系的描述,第三个参数是一个匿名函数,用来定义关联模型的字段。
  • $form->saving() 用来定义一个事件回调,当模型即将保存时会触发这个回调。我们需要在保存商品之前拿到所有 SKU 中最低的价格作为商品的价格,然后通过 $form->model()->price 存入到商品模型中。
  • collect() 函数是 Laravel 提供的一个辅助函数,可以快速创建一个 Collection 对象。在这里我们把用户提交上来的 SKU 数据放到 Collection 中,利用 Collection 提供的 min() 方法求出所有 SKU 中最小的 price,后面的 ?: 0 则是保证当 SKU 数据为空时 price 字段被赋值 0

5.2 CrowdfundingProductsController控制器编辑

在控制器中关于通用部分省略……现在主要说一下$grid()$form()等主要部分代码如下:

<?php

namespace App\Admin\Controllers;
use App\Models\CrowdfundingProduct;
use App\Models\Product;
use App\Http\Controllers\Controller;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;

class CrowdfundingProductsController extends Controller
{
    use HasResourceActions; 

    protected function grid()
    {
        $grid = new Grid(new Product);

        // 只展示 type 为众筹类型的商品
        $grid->model()->where('type', Product::TYPE_CROWDFUNDING);
        $grid->id('ID')->sortable();
        $grid->title('商品名称');
        $grid->on_sale('已上架')->display(function ($value) {
            return $value ? '是' : '否';
        });
        $grid->price('价格');
        // 展示众筹相关字段
        $grid->column('crowdfunding.target_amount', '目标金额');
        $grid->column('crowdfunding.end_at', '结束时间');
        $grid->column('crowdfunding.total_amount', '目前金额');
        $grid->column('crowdfunding.status', ' 状态')->display(function ($value) {
            return CrowdfundingProduct::$statusMap[$value];
        });
           
        return $grid;
    }

    protected function form()
    {
        $form = new Form(new Product);
        // 在表单中添加一个名为 type,值为 Product::TYPE_CROWDFUNDING 的隐藏字段
        $form->hidden('type')->value(Product::TYPE_CROWDFUNDING);
        // 添加众筹相关字段
        $form->text('crowdfunding.target_amount', '众筹目标金额')->rules('required|numeric|min:0.01');
        $form->datetime('crowdfunding.end_at', '众筹结束时间')->rules('required|date');
        $form->hasMany('skus', '商品 SKU', function (Form\NestedForm $form) {
            $form->text('title', 'SKU 名称')->rules('required');
            $form->text('description', 'SKU 描述')->rules('required');
            $form->text('price', '单价')->rules('required|numeric|min:0.01');
            $form->text('stock', '剩余库存')->rules('required|integer|min:0');
        });
        $form->saving(function (Form $form) {
            $form->model()->price = collect($form->input('skus'))->where(Form::REMOVE_FLAG_NAME, 0)->min('price');
        });

        return $form;
    }
}

6.最后配置路由:

app/Admin/routes.php

$router->get('products', 'ProductsController@index');
$router->get('products/{id}/edit', 'ProductsController@edit');
$router->put('products/{id}', 'ProductsController@update');
$router->get('crowdfunding_products', 'CrowdfundingProductsController@index');
$router->get('crowdfunding_products/create', 'CrowdfundingProductsController@create');
$router->post('crowdfunding_products', 'CrowdfundingProductsController@store');
$router->get('crowdfunding_products/{id}/edit', 'CrowdfundingProductsController@edit');
$router->put('crowdfunding_products/{id}', 'CrowdfundingProductsController@update');

最后根据路由在laravel-admin菜单中添加众筹菜单与普通商品菜单 OK 这样就完成了,是不是思路比较清晰了。

阅读 183

Comments