• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

miaoxing / product / 13100246700

02 Feb 2025 03:42PM UTC coverage: 71.116%. Remained the same
13100246700

push

github

semantic-release-bot
chore(release): publish

See CHANGELOG.md for more details.

20 of 31 branches covered (64.52%)

357 of 502 relevant lines covered (71.12%)

25.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.79
/src/Service/ProductModel.php
1
<?php
2

3
namespace Miaoxing\Product\Service;
4

5
use Miaoxing\Category\Service\CategoryModel;
6
use Miaoxing\Logistics\Service\ShippingTplModel;
7
use Miaoxing\Plugin\BaseModel;
8
use Miaoxing\Plugin\Model\HasAppIdTrait;
9
use Miaoxing\Plugin\Model\ModelTrait;
10
use Miaoxing\Plugin\Model\ReqQueryTrait;
11
use Miaoxing\Plugin\Model\SnowflakeTrait;
12
use Wei\Event;
13
use Wei\Model\SoftDeleteTrait;
14
use Wei\Ret;
15
use Wei\Time;
16

17
/**
18
 * 商品模型
19
 *
20
 * @property string|null $id 编号
21
 * @property string $appId 应用编号
22
 * @property string $outerId 外部编号
23
 * @property string $shippingTplId 运费模板编号
24
 * @property string $name 名称
25
 * @property string $intro 简介
26
 * @property string $minPrice 最低的销售价
27
 * @property string $minMarketPrice 最低销售价的划线价
28
 * @property int $minScore 最低的积分
29
 * @property int $stockNum 库存
30
 * @property int $soldNum 销量
31
 * @property string $image 主图
32
 * @property int $status 状态,具体见模型常量
33
 * @property bool $isListing 是否上架
34
 * @property bool $isHidden 是否隐藏不可见
35
 * @property bool $isInList 是否显示在前台列表,根据状态等计算得出
36
 * @property string|null $startAt 开始销售时间
37
 * @property string|null $endAt 结束销售时间
38
 * @property int $maxOrderQuantity 最大购买数量
39
 * @property int $decStockMode 库存计数。1:付款减库存;2:拍下减库存
40
 * @property bool $isAllowAddCart 是否可加入购物车
41
 * @property bool $isAllowCoupon 是否可使用优惠券
42
 * @property bool $isRequireAddress 支付时是否需填写地址
43
 * @property bool $isAllowComment 支付时是否允许留言
44
 * @property int $sort 顺序,从大到小排列
45
 * @property string $configs 配置
46
 * @property string|null $createdAt
47
 * @property string|null $updatedAt
48
 * @property string $createdBy
49
 * @property string $updatedBy
50
 * @property string|null $deletedAt
51
 * @property string $deletedBy
52
 * @property string|null $purgedAt
53
 * @property string $purgedBy
54
 * @property ProductImageModel|ProductImageModel[] $images 商品图片
55
 * @property ProductDetailModel $detail 商品详情
56
 * @property ProductSpecModel $spec 商品规格
57
 * @property SkuModel|SkuModel[] $skus 商品 SKU
58
 * @property CategoryModel|CategoryModel[] $categories 商品分类
59
 * @property CategoriesProductModel|CategoriesProductModel[] $categoriesProducts 商品分类关联
60
 * @property ShippingTplModel $shippingTpl 运费模板
61
 * @property string|null $id 编号
62
 * @property string $appId 应用编号
63
 * @property string $outerId 外部编号
64
 * @property string $shippingTplId 运费模板编号
65
 * @property string $name 名称
66
 * @property string $intro 简介
67
 * @property string $minPrice 最低的销售价
68
 * @property string $minMarketPrice 最低销售价的划线价
69
 * @property int $minScore 最低的积分
70
 * @property int $stockNum 库存
71
 * @property int $soldNum 销量
72
 * @property string $image 主图
73
 * @property int $status 状态,具体见模型常量
74
 * @property bool $isListing 是否上架
75
 * @property bool $isHidden 是否隐藏不可见
76
 * @property bool $isInList 是否显示在前台列表,根据状态等计算得出
77
 * @property string|null $startAt 开始销售时间
78
 * @property string|null $endAt 结束销售时间
79
 * @property int $maxOrderQuantity 最大购买数量
80
 * @property int $decStockMode 库存计数。1:付款减库存;2:拍下减库存
81
 * @property bool $isAllowAddCart 是否可加入购物车
82
 * @property bool $isAllowCoupon 是否可使用优惠券
83
 * @property bool $isRequireAddress 支付时是否需填写地址
84
 * @property bool $isAllowComment 支付时是否允许留言
85
 * @property int $sort 顺序,从大到小排列
86
 * @property string $configs 配置
87
 * @property string|null $createdAt
88
 * @property string|null $updatedAt
89
 * @property string $createdBy
90
 * @property string $updatedBy
91
 * @property string|null $deletedAt
92
 * @property string $deletedBy
93
 * @property string|null $purgedAt
94
 * @property string $purgedBy
95
 */
96
class ProductModel extends BaseModel
97
{
98
    use HasAppIdTrait;
99
    use ModelTrait;
100
    use ReqQueryTrait;
101
    use SnowflakeTrait;
102
    use SoftDeleteTrait;
103

104
    public const STATUS_NOT_STARTED = 1;
105

106
    public const STATUS_ON_SALE = 2;
107

108
    public const STATUS_ENDED = 3;
109

110
    public const STATUS_SOLD_OUT = 4;
111

112
    public const STATUS_DELISTED = 5;
113

114
    public const STATUS_DELETED = 6;
115

116
    /**
117
     * 下单减库存
118
     */
119
    public const DEC_STOCK_MODE_BUY = 1;
120

121
    /**
122
     * 脏腑减库存
123
     */
124
    public const DEC_STOCK_MODE_PAY = 2;
125

126
    protected $deleteStatusColumn = 'status';
127

128
    protected $columns = [
129
        'configs' => [
130
            'cast' => 'array',
131
            'default' => [],
132
        ],
133
    ];
134

135
    /**
136
     * 状态值的详细配置
137
     *
138
     * @var array
139
     */
140
    protected $statusConfigs = [
141
        self::STATUS_NOT_STARTED => [
142
            'name' => '抢购即将开始',
143
            'shortName' => '即将开始',
144
        ],
145
        self::STATUS_ON_SALE => [
146
        ],
147
        self::STATUS_ENDED => [
148
            'name' => '抢购结束',
149
            'shortName' => '已结束',
150
        ],
151
        self::STATUS_SOLD_OUT => [
152
            'name' => '商品已卖光了',
153
            'shortName' => '已售罄',
154
        ],
155
        self::STATUS_DELISTED => [
156
            'name' => '商品已下架',
157
            'shortName' => '已下架',
158
        ],
159
        self::STATUS_DELETED => [
160
            'name' => '商品已删除',
161
            'shortName' => '已删除',
162
        ],
163
    ];
164

165
    public function getGuarded(): array
166
    {
167
        return array_merge($this->guarded, [
78✔
168
            'price',
78✔
169
            'stockNum',
78✔
170
            'soldNum',
78✔
171
        ]);
78✔
172
    }
173

174
    /**
175
     * 商品图片
176
     *
177
     * @return ProductImageModel|ProductImageModel[]
178
     */
179
    public function images(): ProductImageModel
180
    {
181
        return $this->hasMany(ProductImageModel::class);
12✔
182
    }
183

184
    /**
185
     * 商品详情
186
     *
187
     * @return ProductDetailModel
188
     */
189
    public function detail(): ProductDetailModel
190
    {
191
        return $this->hasOne(ProductDetailModel::class);
6✔
192
    }
193

194
    /**
195
     * 商品规格
196
     *
197
     * @return ProductSpecModel
198
     */
199
    public function spec(): ProductSpecModel
200
    {
201
        return $this->hasOne(ProductSpecModel::class);
66✔
202
    }
203

204
    /**
205
     * 商品 SKU
206
     *
207
     * @return SkuModel|SkuModel[]
208
     */
209
    public function skus(): SkuModel
210
    {
211
        return $this->hasMany(SkuModel::class);
66✔
212
    }
213

214
    /**
215
     * 商品分类
216
     *
217
     * @return CategoryModel|CategoryModel[]
218
     */
219
    public function categories(): CategoryModel
220
    {
221
        return $this->belongsToMany(CategoryModel::class);
×
222
    }
223

224
    /**
225
     * 商品分类关联
226
     *
227
     * @return CategoriesProductModel|CategoriesProductModel[]
228
     */
229
    public function categoriesProducts(): CategoriesProductModel
230
    {
231
        return $this->hasMany(CategoriesProductModel::class);
6✔
232
    }
233

234
    /**
235
     * 运费模板
236
     *
237
     * @return ShippingTplModel
238
     */
239
    public function shippingTpl(): ShippingTplModel
240
    {
241
        return $this->belongsTo(ShippingTplModel::class);
×
242
    }
243

244
    /**
245
     * 获取状态配置
246
     *
247
     * @return array
248
     */
249
    public function getStatusConfigs(): array
250
    {
251
        foreach ($this->statusConfigs as $status => &$config) {
30✔
252
            $config['status'] = $status;
30✔
253
        }
254

255
        return $this->statusConfigs;
30✔
256
    }
257

258
    /**
259
     * {@inheritDoc}
260
     */
261
    public function beforeSave()
262
    {
263
        $this->isInList = $this->calIsInList();
114✔
264
    }
265

266
    /**
267
     * {@inheritDoc}
268
     */
269
    public function afterSave()
270
    {
271
        $this->clearTagCache();
114✔
272
//        $this->clearRecordCache();
273
    }
274

275
    /**
276
     * {@inheritDoc}
277
     */
278
    public function afterDestroy()
279
    {
280
        $this->clearTagCache();
12✔
281
//        $this->clearRecordCache();
282
    }
283

284
    /**
285
     * 计算出商品状态并保存
286
     *
287
     * @return $this
288
     */
289
    public function updateStatus(): self
290
    {
291
        $this->status = $this->calStatus();
24✔
292
        $this->save();
24✔
293
        return $this;
24✔
294
    }
295

296
    /**
297
     * 返回默认的规格配置
298
     *
299
     * @return array[][]
300
     * @svc
301
     */
302
    protected function getDefaultSpecs(): array
303
    {
304
        $spec = SpecModel::findByOrCreate(['name' => '默认']);
×
305
        $specValue = SpecValueModel::findByOrCreate(['specId' => $spec->id, 'name' => '默认']);
×
306
        return [
×
307
            $spec->toArray(['id', 'name']) + [
×
308
                'values' => [
×
309
                    $specValue->toArray(['id', 'name']),
×
310
                ],
×
311
            ],
×
312
        ];
×
313
    }
314

315
    /**
316
     * 计算出商品状态
317
     *
318
     * @return int
319
     * @experimental 可能改为保存前自动计算
320
     */
321
    public function calStatus(): int
322
    {
323
        if (!$this->isListing) {
126✔
324
            return static::STATUS_DELISTED;
12✔
325
        }
326

327
        if ($this->stockNum <= 0) {
120✔
328
            return static::STATUS_SOLD_OUT;
24✔
329
        }
330

331
        $now = Time::now();
96✔
332
        if ($this->startAt && $this->startAt > $now) {
96✔
333
            return static::STATUS_NOT_STARTED;
6✔
334
        }
335

336
        if ($this->endAt && $this->endAt < $now) {
90✔
337
            return static::STATUS_ENDED;
18✔
338
        }
339

340
        return static::STATUS_ON_SALE;
72✔
341
    }
342

343
    /**
344
     * 根据状态等计算是否显示在前台列表
345
     *
346
     * @return bool
347
     */
348
    protected function calIsInList(): bool
349
    {
350
        if (
351
            !$this->isListing
114✔
352
            || !$this->isHidden
114✔
353
            || $this->isDeleted()
114✔
354
        ) {
355
            return false;
114✔
356
        }
357
        return true;
×
358
    }
359

360
    /**
361
     * {@inheritDoc}
362
     */
363
    protected function getDeleteStatusValue(): int
364
    {
365
        return static::STATUS_DELETED;
72✔
366
    }
367

368
    /**
369
     * {@inheritDoc}
370
     */
371
    protected function getRestoreStatusValue(): int
372
    {
373
        return $this->calStatus();
×
374
    }
375

376
    /**
377
     * @param string|int|array $categoryId
378
     * @return $this
379
     */
380
    public function withCategoryId($categoryId): self
381
    {
382
        $categoryIds = (array) $categoryId;
×
383
        $subCategories = CategoryModel::select('id')->where('parentId', $categoryId)->fetchAll();
×
384
        $categoryIds = array_merge($categoryIds, array_column($subCategories, 'id'));
×
385

386
        $this->selectMain()
×
387
            ->leftJoinRelation('categoriesProducts')
×
388
            ->where('categoriesProducts.categoryId', $categoryIds);
×
389

390
        return $this;
×
391
    }
392

393
    /**
394
     * 检查商品是否可以加入购物车和下单的相同操作
395
     *
396
     * @return Ret
397
     */
398
    public function checkBeforeCreateCartAndOrder(): Ret
399
    {
400
        if (!in_array($this->status, [static::STATUS_ON_SALE, static::STATUS_NOT_STARTED], true)) {
48✔
401
            $statusConfig = $this->getStatusConfigs()[$this->status];
24✔
402
            return err([
24✔
403
                'message' => $statusConfig['name'],
24✔
404
                'shortMessage' => $statusConfig['shortName'],
24✔
405
            ]);
24✔
406
        }
407

408
        $ret = Event::until('productCheckBeforeCreateCartAndOrder', [$this]);
24✔
409
        if ($ret) {
24✔
410
            return $ret;
×
411
        }
412

413
        return suc();
24✔
414
    }
415

416
    /**
417
     * 检查商品是否可以加入购物车
418
     *
419
     * @param Ret|null $createRet
420
     * @return Ret
421
     */
422
    public function checkCreateCart(?Ret $createRet = null): Ret
423
    {
424
        $createRet || $createRet = $this->checkBeforeCreateCartAndOrder();
48✔
425
        if ($createRet->isErr()) {
48✔
426
            return $createRet;
24✔
427
        }
428

429
        if (!$this->isAllowAddCart) {
24✔
430
            return err('该商品不可加入购物车');
6✔
431
        }
432

433
        $ret = Event::until('productCheckCreateCart', [$this]);
18✔
434
        if ($ret) {
18✔
435
            return $ret;
×
436
        }
437

438
        return suc('可以加入购物车');
18✔
439
    }
440

441
    /**
442
     * 检查商品是否可以下单
443
     *
444
     * @param Ret|null $createRet
445
     * @return Ret
446
     */
447
    public function checkCreateOrder(?Ret $createRet = null): Ret
448
    {
449
        $createRet || $createRet = $this->checkBeforeCreateCartAndOrder();
48✔
450
        if ($createRet->isErr()) {
48✔
451
            return $createRet;
24✔
452
        }
453

454
        if ($this->status === static::STATUS_NOT_STARTED) {
24✔
455
            $statusConfig = $this->getStatusConfigs()[$this->status];
6✔
456
            return err([
6✔
457
                'message' => $statusConfig['name'],
6✔
458
                'shortMessage' => $statusConfig['shortName'],
6✔
459
            ]);
6✔
460
        }
461

462
        $ret = Event::until('productCheckCreateOrder', [$this]);
18✔
463
        if ($ret) {
18✔
464
            return $ret;
×
465
        }
466

467
        return suc('可以购买');
18✔
468
    }
469

470
    /**
471
     * 检查商品是否可以加入购物车或下单
472
     *
473
     * 可用场景
474
     * 1. 商品详情,按需显示加入购物车或立即下单按钮
475
     * 2. SKU 选择器,按需显示加入购物车或立即下单按钮
476
     * 3. 商品列表,按需显示下单按钮,点击后再弹出选择窗口
477
     *
478
     * @return Ret
479
     */
480
    public function checkCreateCartOrOrder(): Ret
481
    {
482
        $create = $this->checkBeforeCreateCartAndOrder();
48✔
483
        $createCart = $this->checkCreateCart($create);
48✔
484
        $createOrder = $this->checkCreateOrder($create);
48✔
485

486
        if ($createCart->isErr() && $createOrder->isErr()) {
48✔
487
            // 如果两者都失败,暂时使用购物车的返回值
488
            $ret = err($createOrder['message'], $createCart['code']);
24✔
489
        } else {
490
            $ret = suc();
24✔
491
        }
492
        $ret['createCart'] = $createCart;
48✔
493
        $ret['createOrder'] = $createOrder;
48✔
494

495
        return $ret;
48✔
496
    }
497
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc