first version

This commit is contained in:
liu
2026-07-23 20:41:25 +08:00
parent 00a0938a1b
commit 10cff754a4
211 changed files with 11577 additions and 842 deletions
@@ -0,0 +1,30 @@
<?php
namespace Database\Factories;
use App\Models\CustomerLevelModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 客户等级工厂(确定性序列数据,无需 Faker)
*
* @extends Factory<CustomerLevelModel>
*/
class CustomerLevelModelFactory extends Factory
{
protected $model = CustomerLevelModel::class;
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'name' => '客户等级' . $seq,
'sort' => $seq,
'status' => CustomerLevelModel::STATUS_NORMAL,
'remark' => '',
];
}
}
@@ -0,0 +1,49 @@
<?php
namespace Database\Factories;
use App\Models\ProductModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 商品工厂(确定性序列数据,无需 Faker)
*
* @extends Factory<ProductModel>
*/
class ProductModelFactory extends Factory
{
protected $model = ProductModel::class;
private const NAMES = ['大白菜', '土豆', '西红柿', '黄瓜', '苹果', '香蕉'];
private const SPECS = ['500g/袋', '10斤/箱', '散装', '25斤/袋'];
private const GRADES = ['特级', '一级', '二级'];
private const UNITS = ['斤', '箱', '袋'];
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'category_id' => 0,
'supplier_id' => 0,
'name' => self::NAMES[$seq % count(self::NAMES)] . $seq,
'spec' => self::SPECS[$seq % count(self::SPECS)],
'grade' => self::GRADES[$seq % count(self::GRADES)],
'unit' => self::UNITS[$seq % count(self::UNITS)],
'image' => '',
'sort' => $seq,
'status' => ProductModel::STATUS_ON,
'remark' => '',
];
}
/**
* 下架商品
*/
public function off(): static
{
return $this->state(fn () => ['status' => ProductModel::STATUS_OFF]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use App\Models\ProductPriceModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 商品等级价格工厂(product_id / level_id 需调用方指定;无需 Faker)
*
* @extends Factory<ProductPriceModel>
*/
class ProductPriceModelFactory extends Factory
{
protected $model = ProductPriceModel::class;
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'product_id' => 0,
'level_id' => 0,
'price' => number_format(random_int(100, 10000) / 100 + $seq * 0.01, 2, '.', ''),
];
}
}
@@ -0,0 +1,43 @@
<?php
namespace Database\Factories;
use App\Models\PurchaseOrderModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 采购单工厂(无需 Faker
*
* @extends Factory<PurchaseOrderModel>
*/
class PurchaseOrderModelFactory extends Factory
{
protected $model = PurchaseOrderModel::class;
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'purchase_no' => 'PO' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT),
'purchase_date' => now()->toDateString(),
'status' => PurchaseOrderModel::STATUS_PENDING,
'total_quantity' => 0,
'total_weight' => 0,
'estimate_amount' => 0,
'actual_amount' => 0,
'operator_id' => 0,
'remark' => '',
];
}
/**
* 指定采购日期
*/
public function onDate(string $date): static
{
return $this->state(fn () => ['purchase_date' => $date]);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Database\Factories;
use App\Models\StoreModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 门店工厂(确定性序列数据,无需 Faker)
*
* @extends Factory<StoreModel>
*/
class StoreModelFactory extends Factory
{
protected $model = StoreModel::class;
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'name' => '测试门店' . $seq,
'code' => 'S' . str_pad((string) $seq, 6, '0', STR_PAD_LEFT),
'level_id' => 0,
'contact' => '联系人' . $seq,
'phone' => '138' . str_pad((string) $seq, 8, '0', STR_PAD_LEFT),
'address' => '测试地址' . $seq . '号',
'payment_cycle_days' => $seq % 8,
'status' => StoreModel::STATUS_NORMAL,
'remark' => '',
];
}
/**
* 停用门店
*/
public function disabled(): static
{
return $this->state(fn () => ['status' => StoreModel::STATUS_DISABLED]);
}
/**
* 指定回款周期(天)
*/
public function paymentCycle(int $days): static
{
return $this->state(fn () => ['payment_cycle_days' => $days]);
}
}
@@ -0,0 +1,42 @@
<?php
namespace Database\Factories;
use App\Models\StoreOrderItemModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 门店订单明细工厂(order_id / store_id / product_id 需调用方指定;
* amount 未显式指定时按 price × quantity 自动计算;无需 Faker
*
* @extends Factory<StoreOrderItemModel>
*/
class StoreOrderItemModelFactory extends Factory
{
protected $model = StoreOrderItemModel::class;
public function definition(): array
{
return [
'order_id' => 0,
'store_id' => 0,
'product_id' => 0,
'product_name' => '测试商品',
'product_spec' => '500g/袋',
'price' => number_format(random_int(100, 5000) / 100, 2, '.', ''),
'quantity' => number_format(random_int(100, 10000) / 100, 2, '.', ''),
'weight' => 0,
'amount' => 0,
'remark' => '',
];
}
public function configure(): static
{
return $this->afterMaking(function (StoreOrderItemModel $item): void {
if ((float) $item->amount === 0.0 && (float) $item->price > 0 && (float) $item->quantity > 0) {
$item->amount = bcmul((string) $item->price, (string) $item->quantity, 2);
}
});
}
}
@@ -0,0 +1,50 @@
<?php
namespace Database\Factories;
use App\Models\StoreOrderModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 门店订单工厂(store_id 需调用方指定;无需 Faker)
*
* @extends Factory<StoreOrderModel>
*/
class StoreOrderModelFactory extends Factory
{
protected $model = StoreOrderModel::class;
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'order_no' => 'SO' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT),
'store_id' => 0,
'order_date' => now()->toDateString(),
'total_quantity' => 0,
'total_weight' => 0,
'total_amount' => 0,
'status' => StoreOrderModel::STATUS_PENDING,
'remark' => '',
];
}
/**
* 指定订货日期
*/
public function onDate(string $date): static
{
return $this->state(fn () => ['order_date' => $date]);
}
/**
* 已汇总(已被采购单归集)
*/
public function summarized(): static
{
return $this->state(fn () => ['status' => StoreOrderModel::STATUS_SUMMARIZED]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Database\Factories;
use App\Models\SupplierModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 供应商工厂(确定性序列数据,无需 Faker)
*
* @extends Factory<SupplierModel>
*/
class SupplierModelFactory extends Factory
{
protected $model = SupplierModel::class;
private const MAIN_PRODUCTS = ['蔬菜', '水果', '蔬菜/水果', '肉禽蛋'];
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'name' => '测试供应商' . $seq,
'contact' => '联系人' . $seq,
'phone' => '139' . str_pad((string) $seq, 8, '0', STR_PAD_LEFT),
'address' => '供应商地址' . $seq . '号',
'main_products' => self::MAIN_PRODUCTS[$seq % count(self::MAIN_PRODUCTS)],
'status' => SupplierModel::STATUS_NORMAL,
'remark' => '',
];
}
/**
* 停用供应商
*/
public function disabled(): static
{
return $this->state(fn () => ['status' => SupplierModel::STATUS_DISABLED]);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace Database\Factories;
use App\Models\UserModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 小程序用户工厂(微信登录自动生成,供测试使用;无需 Faker)
*
* @extends Factory<UserModel>
*/
class UserModelFactory extends Factory
{
protected $model = UserModel::class;
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'username' => null,
'password' => null,
'nickname' => '微信用户' . $seq,
'email' => '',
'openid' => 'openid_' . str_pad((string) $seq, 16, '0', STR_PAD_LEFT),
'unionid' => '',
'phone' => '',
'avatar' => '',
'type' => UserModel::TYPE_PENDING,
'store_id' => 0,
'supplier_id' => 0,
'status' => UserModel::STATUS_NORMAL,
'last_login_at' => null,
];
}
/**
* 已绑定门店的门店用户
*/
public function forStore(int $storeId): static
{
return $this->state(fn () => [
'type' => UserModel::TYPE_STORE,
'store_id' => $storeId,
]);
}
/**
* 已绑定供应商的供应商用户
*/
public function forSupplier(int $supplierId): static
{
return $this->state(fn () => [
'type' => UserModel::TYPE_SUPPLIER,
'supplier_id' => $supplierId,
]);
}
/**
* 停用账号
*/
public function disabled(): static
{
return $this->state(fn () => ['status' => UserModel::STATUS_DISABLED]);
}
}
@@ -22,7 +22,7 @@ return new class extends Migration
$table->decimal('total_quantity', 10, 2)->default(0)->comment('订货总量');
$table->decimal('total_weight', 10, 3)->default(0)->comment('总重量');
$table->decimal('total_amount', 10, 2)->default(0)->comment('订单总金额');
$table->integer('status')->default(0)->comment('订单状态(0待汇总 1已汇总 2配送中 3已完成 4已取消)');
$table->integer('status')->default(0)->comment('订单状态(0待汇总 1已汇总 2配送中 3已完成 9已取消)');
$table->string('remark', 255)->default('')->comment('订单备注');
$table->timestamps();
$table->index(['store_id', 'order_date'], 'store_order_store_date_index');
@@ -18,7 +18,7 @@ return new class extends Migration
$table->increments('id')->comment('采购单ID');
$table->string('purchase_no', 32)->unique()->comment('采购单编号');
$table->date('purchase_date')->comment('采购日期');
$table->integer('status')->default(0)->comment('采购单状态(0草稿 1已确认 2部分发送 3全部发送 4已完成)');
$table->integer('status')->default(0)->comment('采购单状态(0待发送 1部分发送 2全部发送 3已完成)');
$table->decimal('total_quantity', 10, 2)->default(0)->comment('采购总量');
$table->decimal('total_weight', 10, 3)->default(0)->comment('采购总重量');
$table->decimal('estimate_amount', 10, 2)->default(0)->comment('预估金额(按订货汇总)');
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 待确认事项 #7(已批准):供应商小程序确认接单状态落库
*/
public function up(): void
{
if (! Schema::hasColumn('purchase_order_item', 'supplier_confirmed_at')) {
Schema::table('purchase_order_item', function (Blueprint $table) {
$table->timestamp('supplier_confirmed_at')->nullable()->after('sent_at')->comment('供应商确认接单时间(NULL未确认)');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasColumn('purchase_order_item', 'supplier_confirmed_at')) {
Schema::table('purchase_order_item', function (Blueprint $table) {
$table->dropColumn('supplier_confirmed_at');
});
}
}
};
+276
View File
@@ -0,0 +1,276 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
/**
* 订货采购系统业务菜单与权限点种子
*
* 约定(V2.0 计划):
* - local 一律留空,name 直接写中文(layout 在 local 为空时回退显示 name,无需 i18n)
* - 幂等:顶级菜单 key 已存在时跳过,可重复执行
* - 执行后自动将全部新增权限点授予超级管理员角色(role_id=1)
*
* 执行:php artisan db:seed --class=ProcurementSeeder
*/
class ProcurementSeeder extends Seeder
{
/**
* 菜单结构:menu 目录 → route 页面 → rule 权限点
*/
private function menus(): array
{
return [
[
'type' => 'menu',
'key' => 'procurement.product',
'name' => '商品中心',
'icon' => 'ShoppingOutlined',
'children' => [
[
'type' => 'route',
'key' => 'product.category',
'name' => '分类管理',
'path' => '/product/category',
'children' => [
['type' => 'rule', 'key' => 'product.category.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'product.category.create', 'name' => '新增'],
['type' => 'rule', 'key' => 'product.category.update', 'name' => '编辑'],
['type' => 'rule', 'key' => 'product.category.delete', 'name' => '删除'],
],
],
[
'type' => 'route',
'key' => 'product.goods',
'name' => '商品列表',
'path' => '/product/goods',
'children' => [
['type' => 'rule', 'key' => 'product.goods.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'product.goods.create', 'name' => '新增'],
['type' => 'rule', 'key' => 'product.goods.update', 'name' => '编辑'],
['type' => 'rule', 'key' => 'product.goods.delete', 'name' => '删除'],
['type' => 'rule', 'key' => 'product.goods.batchPrice', 'name' => '批量调价'],
],
],
],
],
[
'type' => 'menu',
'key' => 'procurement.customer',
'name' => '客户管理',
'icon' => 'ShopOutlined',
'children' => [
[
'type' => 'route',
'key' => 'customer.store',
'name' => '门店管理',
'path' => '/customer/store',
'children' => [
['type' => 'rule', 'key' => 'customer.store.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'customer.store.create', 'name' => '新增'],
['type' => 'rule', 'key' => 'customer.store.update', 'name' => '编辑'],
['type' => 'rule', 'key' => 'customer.store.delete', 'name' => '删除'],
],
],
[
'type' => 'route',
'key' => 'customer.level',
'name' => '客户等级',
'path' => '/customer/level',
'children' => [
['type' => 'rule', 'key' => 'customer.level.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'customer.level.create', 'name' => '新增'],
['type' => 'rule', 'key' => 'customer.level.update', 'name' => '编辑'],
['type' => 'rule', 'key' => 'customer.level.delete', 'name' => '删除'],
],
],
[
'type' => 'route',
'key' => 'customer.supplier',
'name' => '供应商',
'path' => '/customer/supplier',
'children' => [
['type' => 'rule', 'key' => 'customer.supplier.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'customer.supplier.create', 'name' => '新增'],
['type' => 'rule', 'key' => 'customer.supplier.update', 'name' => '编辑'],
['type' => 'rule', 'key' => 'customer.supplier.delete', 'name' => '删除'],
],
],
[
'type' => 'route',
'key' => 'customer.miniUser',
'name' => '小程序用户',
'path' => '/customer/mini-user',
'children' => [
['type' => 'rule', 'key' => 'customer.miniUser.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'customer.miniUser.update', 'name' => '启用停用'],
['type' => 'rule', 'key' => 'customer.miniUser.bind', 'name' => '绑定主体'],
],
],
[
'type' => 'route',
'key' => 'customer.notice',
'name' => '通知管理',
'path' => '/customer/notice',
'children' => [
['type' => 'rule', 'key' => 'customer.notice.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'customer.notice.create', 'name' => '发布'],
['type' => 'rule', 'key' => 'customer.notice.delete', 'name' => '删除'],
],
],
],
],
[
'type' => 'menu',
'key' => 'procurement.order',
'name' => '订货管理',
'icon' => 'FileTextOutlined',
'children' => [
[
'type' => 'route',
'key' => 'order.store',
'name' => '门店订单',
'path' => '/order/store',
'children' => [
['type' => 'rule', 'key' => 'order.store.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'order.store.update', 'name' => '状态流转'],
],
],
],
],
[
'type' => 'menu',
'key' => 'procurement.purchase',
'name' => '采购管理',
'icon' => 'ShoppingCartOutlined',
'children' => [
[
'type' => 'route',
'key' => 'purchase.order',
'name' => '采购单',
'path' => '/purchase/order',
'children' => [
['type' => 'rule', 'key' => 'purchase.order.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'purchase.order.update', 'name' => '修改'],
['type' => 'rule', 'key' => 'purchase.order.generate', 'name' => '生成采购单'],
['type' => 'rule', 'key' => 'purchase.order.export', 'name' => '导出'],
['type' => 'rule', 'key' => 'purchase.order.send', 'name' => '发送供应商'],
['type' => 'rule', 'key' => 'purchase.order.allocate', 'name' => '金额分摊'],
],
],
],
],
[
'type' => 'menu',
'key' => 'procurement.recon',
'name' => '对账管理',
'icon' => 'AccountBookOutlined',
'children' => [
[
'type' => 'route',
'key' => 'recon.list',
'name' => '财务对账',
'path' => '/recon/list',
'children' => [
['type' => 'rule', 'key' => 'recon.list.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'recon.list.create', 'name' => '新增'],
['type' => 'rule', 'key' => 'recon.list.update', 'name' => '编辑'],
['type' => 'rule', 'key' => 'recon.list.delete', 'name' => '删除'],
['type' => 'rule', 'key' => 'recon.list.build', 'name' => '生成明细'],
['type' => 'rule', 'key' => 'recon.list.settle', 'name' => '生成结算表'],
// 对账明细操作权限点在独立控制器 recon.item 下(D4/D6/D8
['type' => 'rule', 'key' => 'recon.item.item.update', 'name' => '对账明细操作'],
],
],
[
'type' => 'route',
'key' => 'recon.statement',
'name' => '门店对账单',
'path' => '/recon/statement',
'children' => [
['type' => 'rule', 'key' => 'recon.statement.query', 'name' => '查询'],
],
],
[
'type' => 'route',
'key' => 'recon.settlement',
'name' => '结算表',
'path' => '/recon/settlement',
'children' => [
['type' => 'rule', 'key' => 'recon.settlement.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'recon.settlement.download', 'name' => '下载导出'],
],
],
],
],
];
}
public function run(): void
{
$existing = DB::table('sys_rule')
->whereIn('key', ['procurement.product', 'procurement.customer', 'procurement.order', 'procurement.purchase', 'procurement.recon'])
->exists();
if ($existing) {
$this->command?->warn('业务菜单已存在,跳过 ProcurementSeeder');
return;
}
DB::transaction(function () {
$ruleIds = [];
foreach ($this->menus() as $menu) {
$this->insertNode($menu, 0, $ruleIds);
}
// 授权给超级管理员角色(role_id=1),去重已存在的关联
$existingPairs = DB::table('sys_role_rule')
->where('role_id', 1)
->whereIn('rule_id', $ruleIds)
->pluck('rule_id')
->all();
$newPairs = array_map(
static fn (int $ruleId) => ['role_id' => 1, 'rule_id' => $ruleId],
array_values(array_diff($ruleIds, $existingPairs))
);
if ($newPairs !== []) {
DB::table('sys_role_rule')->insert($newPairs);
}
$this->command?->info('业务菜单与权限点已写入,共 ' . count($ruleIds) . ' 个权限点已授权给超级管理员');
});
}
/**
* 递归写入菜单节点,收集全部节点 ID(menu/route/rule)用于授权
*
* @param array<string, mixed> $node
* @param array<int> $ruleIds
*/
private function insertNode(array $node, int $parentId, array &$ruleIds): void
{
$now = now();
$id = DB::table('sys_rule')->insertGetId([
'parent_id' => $parentId,
'type' => $node['type'],
'key' => $node['key'],
'name' => $node['name'],
'path' => $node['path'] ?? '',
'icon' => $node['icon'] ?? '',
'order' => 0,
'local' => '',
'status' => 1,
'hidden' => 1,
'link' => 0,
'created_at' => $now,
'updated_at' => $now,
]);
$ruleIds[] = $id;
foreach ($node['children'] ?? [] as $child) {
$this->insertNode($child, $id, $ruleIds);
}
}
}