init
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
---
|
||||
description: REST API 路由与统一 JSON 返回格式
|
||||
globs: backend/routes/**/*.php,backend/app/Http/**/*.php
|
||||
alwaysApply: false
|
||||
---
|
||||
# API 规范
|
||||
|
||||
详见 `docs/07-api.md`。
|
||||
|
||||
## 路由
|
||||
- 统一前缀 `/api/v1/`,资源名复数,路由参数优先业务编码。
|
||||
- 状态动作:`POST /api/v1/{resource}/{code}/{action}`
|
||||
|
||||
```
|
||||
POST /api/v1/inbounds/{inbound_no}/receive
|
||||
POST /api/v1/outbounds/{outbound_no}/allocate
|
||||
```
|
||||
|
||||
- 认证 Sanctum;每个接口都要经过 Policy / Gate 授权,权限码形如 `outbound:allocate`。
|
||||
- 列表接口必须分页并使用统一分页格式。
|
||||
|
||||
## 统一返回
|
||||
所有接口通过统一响应工具(如 `App\Support\ApiResponse`)返回,禁止 Controller 自定义结构。
|
||||
|
||||
```json
|
||||
{ "success": true, "message": "OK", "data": {} }
|
||||
{ "success": false, "message": "操作失败", "errors": {} }
|
||||
```
|
||||
|
||||
- 输出数据统一经 API Resource 转换,不直接返回 Model。
|
||||
- 验证错误、业务异常、授权失败在异常处理器中统一转换为失败格式。
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: MySQL 表设计、迁移与索引规范
|
||||
globs: backend/database/**/*.php,backend/app/Models/**/*.php,docs/database/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# 数据库规范
|
||||
|
||||
详见 `docs/09-database.md`。表结构设计存放在 `docs/database/`(索引 `docs/database/README.md`)。
|
||||
|
||||
- **设计先行**:先在 `docs/database/` 中设计并确认表结构,再写迁移;迁移与设计文档必须保持一致,改表时同步更新文档。
|
||||
|
||||
- InnoDB + utf8mb4;所有表有 `created_at`、`updated_at`。
|
||||
- 核心业务表必须有 `tenant_code`;业务编码唯一约束包含租户:`UNIQUE (tenant_code, sku_code)`。
|
||||
- 重要业务表按需加 `status`、`version`(乐观锁)、`deleted_at`;**不要无脑给所有表加软删除**,流水类事实表禁止软删除与更新。
|
||||
- 数量字段使用整数或 `DECIMAL`,禁止 `FLOAT/DOUBLE`;金额使用 `DECIMAL`。
|
||||
- 索引围绕实际查询设计,复合索引通常以 `tenant_code` 为最左列;关注 `warehouse_code`、`customer_code`、`sku_code`、`order_no`、`inbound_no`、`outbound_no`、`location_code`、`status`、`created_at`。
|
||||
- 迁移必须可回滚(实现 `down()`),已上线的迁移不修改,新增迁移变更。
|
||||
- 表名是单数加 `wms_` 前缀(如 `wms_sku`、`wms_stock_log`)。每张表一个 SQL 文件,放在 `docs/database/` 对应领域目录,约定见 `docs/database/00-conventions.md`。
|
||||
|
||||
```php
|
||||
// ✅
|
||||
$table->string('tenant_code', 32);
|
||||
$table->string('sku_code', 64);
|
||||
$table->unique(['tenant_code', 'sku_code']);
|
||||
$table->index(['tenant_code', 'status', 'created_at']);
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: 分阶段开发流程、修改前后检查清单与禁止事项
|
||||
alwaysApply: true
|
||||
---
|
||||
# 开发工作方式
|
||||
|
||||
像资深项目开发人员一样工作,而不是代码生成器。详见 `docs/15-development-workflow.md`、`docs/16-roadmap.md`。
|
||||
|
||||
## 分阶段
|
||||
- 严格按 Phase 1~17 顺序推进(`docs/16-roadmap.md`),不要一次生成整个项目或几百个文件。
|
||||
- 每个 Phase 开始前先说明:目标、涉及模块、数据库变化、API 变化、前端变化、测试方案、风险;确认后再实施。
|
||||
- 阶段完成后:检查代码 → 数据库 → 运行测试 → API → 前端 → 修复问题,再进入下一阶段。
|
||||
|
||||
## 修改前
|
||||
理解目录 → 搜索已有实现(不重复造)→ 检查数据库结构、相关 API、前端调用 → 再修改。
|
||||
|
||||
## 修改后
|
||||
检查类型、语法、引用、迁移、API、权限、**租户隔离**,并运行相关测试。
|
||||
|
||||
## 禁止
|
||||
- 跳过架构设计;Controller 堆业务;一个 Model 承担所有业务
|
||||
- 忽略 `tenant_code`;直接改库存不留流水;不考虑并发库存
|
||||
- Python 直接修改核心 WMS 数据;平台逻辑写死进核心订单模型
|
||||
- 过早微服务化;没有测试就继续堆功能;随意增加第三方依赖
|
||||
- 重复实现 Element Plus / Admin 框架已提供的能力
|
||||
- 魔法字符串、硬编码、重复 SQL、跨模块强耦合
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
description: Docker 开发环境规范
|
||||
globs: docker-compose*.yml,docker/**,**/Dockerfile
|
||||
alwaysApply: false
|
||||
---
|
||||
# Docker 开发环境
|
||||
|
||||
详见 `docs/13-docker.md`。
|
||||
|
||||
- 根目录**一个** `docker-compose.yml`,不为小模块建立独立 Compose。
|
||||
- 服务:php-fpm(PHP 8.4)、nginx、mysql(8)、redis(7)、node(前端开发)、py-service(FastAPI,构建上下文 `py-service/`)。
|
||||
- Queue Worker、Scheduler 为独立服务,复用 PHP 镜像。
|
||||
- 镜像定义放 `docker/{service}/`;敏感配置经 `.env` 注入,不写入镜像与 Git。
|
||||
- 数据卷持久化 MySQL / Redis;端口、密码可通过 `.env` 配置。
|
||||
- 第一阶段不引入 Kubernetes,生产方案在 Phase 17 设计。
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: Vue 3 + TypeScript + Element Plus 前端规范(pure-admin-thin)
|
||||
globs: frontend/**/*.vue,frontend/**/*.ts,frontend/**/*.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
# 前端规范
|
||||
|
||||
详见 `docs/10-frontend.md`。
|
||||
|
||||
## 框架
|
||||
- Admin 基于 pure-admin-thin(i18n 分支),复用其 Layout / Router / 权限 / 标签页 / i18n,不重复实现。
|
||||
- UI 只用 Element Plus,不引入 Naive UI 等第二套组件库;不重复实现 Element Plus 已有组件。
|
||||
- 新增依赖前先确认框架与现有依赖无法满足。
|
||||
|
||||
## 结构
|
||||
- 页面按业务领域放 `src/views/{domain}/`(warehouse、inventory、inbound、outbound 等)。
|
||||
- 接口调用统一放 `src/api/{domain}.ts`,通过框架封装的 HTTP 实例,页面内不直接写 axios。
|
||||
- 类型放 `src/types/`,状态 / 枚举常量放 `src/constants/`,禁止魔法字符串。
|
||||
|
||||
## 编码
|
||||
- `<script setup lang="ts">` + Composition API;Props、Emits、接口响应都要声明类型,避免 `any`。
|
||||
- 列表页统一:查询区 → Toolbar → ElTable → ElPagination;表单用 ElForm + rules,弹窗用 ElDialog / ElDrawer。
|
||||
- 文案走 i18n,不硬编码中文。
|
||||
|
||||
## 权限
|
||||
- 按钮 / 菜单权限使用权限码 `模块:动作`(如 `inventory:adjust`、`outbound:ship`),通过框架的权限指令 / 组件控制。
|
||||
- 前端权限只控制展示,最终以后端授权为准。
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
description: 库存模型、库存流水与并发安全规则(任何涉及库存数量变化的代码)
|
||||
globs: backend/app/**/Inventory/**,backend/app/**/Inbound/**,backend/app/**/Outbound/**,backend/app/**/Picking/**,backend/app/**/Return/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# 库存规则
|
||||
|
||||
详见 `docs/05-inventory.md`。库存是整个 WMS 的核心。
|
||||
|
||||
## 模型
|
||||
- 维度:仓库 → 库区 → 库位 → SKU → 批次 → 库存状态,不要简化为 `sku + warehouse + quantity`。
|
||||
- 数量:`available_qty`、`reserved_qty`、`picked_qty`、`hold_qty`、`damaged_qty`、`in_transit_qty`。
|
||||
|
||||
## 流水
|
||||
- 任何库存数量变化,必须在**同一事务**内写入 `wms_stock_log`(变化量、变化前后、业务单据类型与单号、操作人)。
|
||||
- 库存变更只能通过库存领域服务 / Action 完成,业务模块不得直接 update 库存表。
|
||||
- 流水只追加,不修改、不删除。
|
||||
|
||||
## 并发
|
||||
禁止 "先 SELECT → PHP 计算 → UPDATE"。使用事务 + 原子条件更新 / 行锁 / 乐观锁,必要时 Redis 锁。
|
||||
|
||||
```php
|
||||
// ❌ 并发下会超卖
|
||||
$inv = Inventory::find($id);
|
||||
if ($inv->available_qty >= $qty) { $inv->available_qty -= $qty; $inv->save(); }
|
||||
|
||||
// ✅ 原子条件更新,受影响行数为 0 即库存不足
|
||||
$affected = Inventory::whereKey($id)
|
||||
->where('available_qty', '>=', $qty)
|
||||
->update([
|
||||
'available_qty' => DB::raw('available_qty - '.(int) $qty),
|
||||
'reserved_qty' => DB::raw('reserved_qty + '.(int) $qty),
|
||||
]);
|
||||
if ($affected === 0) { throw new InsufficientStockException(); }
|
||||
```
|
||||
|
||||
- 多行扣减按固定顺序(如库存主键升序)加锁,避免死锁。
|
||||
- 必须有并发测试:库存 10,两个请求同时扣 8,只能成功一个,且不会出现负库存。
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
description: Laravel 后端分层、目录结构与编码规范
|
||||
globs: backend/**/*.php
|
||||
alwaysApply: false
|
||||
---
|
||||
# Laravel 后端规范
|
||||
|
||||
详见 `docs/06-backend-laravel.md`。
|
||||
|
||||
## 分层
|
||||
Controller → FormRequest → Application Action/Service → Domain → Model → DB
|
||||
|
||||
- Controller 只做:接收请求、调用 FormRequest 验证、调用 Action/Service、返回 API Resource。
|
||||
- 复杂业务放 `app/Application/{Module}/Actions`,一个 Action 一个用例(如 `AllocateOutboundAction`)。
|
||||
- 业务规则、状态机、Enum、值对象放 `app/Domain/{Module}`,不依赖 Http 与第三方平台。
|
||||
- 第三方平台 / 物流商实现放 `app/Infrastructure/`;租户上下文、统一响应等放 `app/Support/`。
|
||||
- 事务边界在 Application 层;跨模块协作走 Application Service 或 Events。
|
||||
|
||||
```php
|
||||
// ❌ Controller 中直接写业务与查询
|
||||
public function ship(string $outboundNo) {
|
||||
$o = Outbound::where('outbound_no', $outboundNo)->first();
|
||||
if ($o->status == 'packed') { $o->status = 'shipped'; $o->save(); }
|
||||
}
|
||||
|
||||
// ✅
|
||||
public function ship(ShipOutboundRequest $request, string $outboundNo, ShipOrderAction $action) {
|
||||
return ApiResponse::success(new OutboundResource($action->execute($outboundNo, $request->toDto())));
|
||||
}
|
||||
```
|
||||
|
||||
## 编码
|
||||
- `declare(strict_types=1);`,参数与返回值写明类型;DTO 在层间传递,不传裸数组。
|
||||
- 状态、类型一律用 PHP Enum,非法状态转换抛业务异常。
|
||||
- 授权用 Policies / Gates;认证用 Sanctum。
|
||||
- 避免超大 Controller / Service;不重复 SQL,查询复用 Scope。
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: WMS SaaS 项目核心架构约束与文档索引
|
||||
alwaysApply: true
|
||||
---
|
||||
# 项目核心约束
|
||||
|
||||
完整规范见 `docs/`(索引 `docs/README.md`)。涉及某领域的改动前,先阅读对应文档。
|
||||
|
||||
## 仓库结构(monorepo)
|
||||
- `backend/`:Laravel 13 + PHP 8.4,唯一的核心业务系统
|
||||
- `frontend/`:基于 pure-admin-thin(i18n 分支),Vue 3 + TS + Vite + Element Plus
|
||||
- `py-service/`:Python FastAPI,独立 AI / 数据分析服务
|
||||
- `docs/`:设计文档;`docs/database/` 存放数据库表结构设计
|
||||
- 根目录一个 `docker-compose.yml`
|
||||
|
||||
## 不可违背的架构红线
|
||||
1. **多租户**:单库 + `tenant_code`,通过 TenantContext 自动隔离;严禁跨租户读写。→ `docs/03-multi-tenancy.md`
|
||||
2. **库存**:余额是结果,流水(`wms_stock_log`)是事实;任何库存变化必须同事务写流水,且并发安全。→ `docs/05-inventory.md`
|
||||
3. **Laravel 是唯一核心业务系统**:Python 只做分析,只通过 `/internal/v1/` API 交互,不直接写核心库。→ `docs/11-py-service.md`
|
||||
4. **平台解耦**:订单为统一模型,第三方平台走 Connector/Adapter,核心表不写死平台逻辑。→ `docs/04-domain-model.md`
|
||||
5. **状态机**:状态用 Enum + 集中定义的合法转换,禁止散落的 `if ($status == ...)`。
|
||||
6. **业务编码优先**:业务关联优先使用 `sku_code`、`outbound_no` 等,自增 ID 仅作内部主键。
|
||||
7. **MySQL 是事实数据源**,Redis 只做 Cache / Queue / Lock / Rate Limit。
|
||||
8. **不过度设计**:第一阶段不引入 K8s、Kafka、ES、MongoDB、ClickHouse、微服务。
|
||||
|
||||
## 架构问题处理
|
||||
需求存在架构问题时不要直接照做,先说明:问题 → 原因 → 推荐方案 → 替代方案 → 最终建议。
|
||||
存在多个影响全局的合理方案时,先说明区别,由用户决定。
|
||||
|
||||
整个项目遵循:**简单、清晰、可维护、可扩展、业务优先。**
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
description: Python FastAPI AI / 数据分析服务边界与规范
|
||||
globs: py-service/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# Python Service(py-service)
|
||||
|
||||
详见 `docs/11-py-service.md`。
|
||||
|
||||
## 边界(红线)
|
||||
- 只负责:AI 分析、库存分析、销量 / 缺货预测、补货建议、异常检测、AI 助手、机器学习。
|
||||
- **不实现**用户、租户、SKU、仓库、库存、入库、出库等核心业务逻辑。
|
||||
- **不直接连接或写入** Laravel 核心业务数据库;数据由 Laravel 推送,或通过 Laravel 只读接口获取。
|
||||
- 分析结果返回给 Laravel,由 Laravel 决定是否落库或转化为业务动作。
|
||||
|
||||
## 接口
|
||||
- 前缀 `/internal/v1/`,如 `POST /internal/v1/inventory/forecast`。
|
||||
- 内部鉴权(Token / 签名),不对公网暴露。
|
||||
- 请求必须带 `tenant_code`,缓存与结果按租户隔离。
|
||||
|
||||
## 结构与编码
|
||||
```
|
||||
py-service/app/
|
||||
├── main.py api/ services/ schemas/ models/
|
||||
├── analysis/ ai/ tasks/ core/
|
||||
```
|
||||
- 路由在 `api/`,业务在 `services/`,请求响应模型用 Pydantic 定义在 `schemas/`。
|
||||
- 全部函数写类型注解;配置用 pydantic-settings 从环境变量读取,密钥不入库不入 Git。
|
||||
- 重型依赖(PyTorch 等)按需引入,不提前加入。
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
description: Laravel Queue Job 与 Scheduler 规范
|
||||
globs: backend/app/Jobs/**,backend/app/Console/**,backend/app/Listeners/**,backend/routes/console.php
|
||||
alwaysApply: false
|
||||
---
|
||||
# Queue / Scheduler
|
||||
|
||||
详见 `docs/08-queue-scheduler.md`。
|
||||
|
||||
## 必须入队的任务
|
||||
第三方订单 / 库存 / 物流 / Tracking 同步、批量导入导出、面单生成、大量库存计算、报表生成、AI 分析、邮件通知、Webhook 处理。
|
||||
**用户 HTTP 请求不得同步等待第三方 API。**
|
||||
|
||||
## Job 要求
|
||||
- 构造参数携带 `tenant_code`,`handle()` 开始时恢复 TenantContext。
|
||||
- 幂等:重复执行不重复建单、不重复扣库存(用业务唯一键或幂等键判断)。
|
||||
- 显式设置 `$tries`、`$backoff`、`$timeout`,失败记录日志(`failed()`)。
|
||||
- 传递业务编码 / 主键,不序列化大对象。
|
||||
|
||||
## Scheduler 要求
|
||||
- 任务可配置(开关、频率在配置或租户设置中)。
|
||||
- 使用 `withoutOverlapping()` / `onOneServer()` 防重复执行。
|
||||
- 设置超时、失败重试与日志;按租户分发 Job,保证租户隔离。
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# 身份设定
|
||||
你现在是一名资深 SaaS / WMS 系统架构师、Laravel 后端工程师、Vue 3 前端工程师,同时具备仓储物流、ERP、3PL、跨境电商仓储业务经验。
|
||||
|
||||
我要从零开发一个商业化、多租户 SaaS WMS(Warehouse Management System)。
|
||||
|
||||
请不要把它当成简单的 CRUD 后台,而要按照一个未来可以长期商业化、支持多个租户、多个仓库、3PL/海外仓、电商订单接入的正式 SaaS 产品进行设计和开发。
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
description: 安全、审计日志与密钥管理规则;处理认证、权限、上传、Webhook、日志、第三方凭证时使用
|
||||
globs: backend/app/**/*.php,backend/config/**/*.php,backend/routes/**/*.php
|
||||
alwaysApply: false
|
||||
---
|
||||
# 安全与审计
|
||||
|
||||
详见 `docs/12-security-audit.md`。
|
||||
|
||||
## 审计
|
||||
- 库存、订单、入库、出库状态变更及用户权限变更,必须记录:操作人、时间、动作、修改前、修改后、来源 IP、请求 ID。
|
||||
- 记录登录日志、操作日志、API 日志;请求 ID 贯穿 HTTP → Job → 外部调用。
|
||||
|
||||
## 安全
|
||||
- 查询只用 Eloquent / Query Builder 参数绑定;禁止拼接用户输入到 `DB::raw`。
|
||||
- 所有接口:Sanctum 认证 + Policy 授权 + 租户隔离 + 必要的 Rate Limit。
|
||||
- 文件上传校验类型、大小、扩展名,存储于非公开目录,文件名重新生成。
|
||||
- 第三方 Webhook 必须校验签名与时间戳,防重放。
|
||||
|
||||
## 密钥
|
||||
- Token / Secret **禁止写入 Git**:使用 `.env`,`.env.example` 只放占位。
|
||||
- 租户级第三方凭证使用 Laravel 加密(`encrypted` cast)存储。
|
||||
- 日志脱敏:不输出密码、Token、完整手机号 / 地址等敏感信息。
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: 多租户 tenant_code 隔离规则(后端模型、查询、任务、缓存)
|
||||
globs: backend/app/**/*.php,backend/database/**/*.php,backend/routes/**/*.php
|
||||
alwaysApply: false
|
||||
---
|
||||
# 租户隔离
|
||||
|
||||
详见 `docs/03-multi-tenancy.md`。**严禁租户 A 查询或修改租户 B 的数据。**
|
||||
|
||||
- 方案:单数据库 + `tenant_code`;所有核心业务表必须有 `tenant_code`。
|
||||
- 通过 TenantContext + 全局 Scope / Trait 自动过滤查询、自动填充 `tenant_code`,不要到处手写 where。
|
||||
|
||||
```php
|
||||
// ❌ 手工拼接,容易遗漏
|
||||
Sku::where('tenant_code', $request->user()->tenant_code)->get();
|
||||
|
||||
// ✅ 模型使用租户 Trait,查询自动限定当前租户
|
||||
Sku::query()->get();
|
||||
```
|
||||
|
||||
- 绕过租户 Scope(`withoutGlobalScope`)只允许出现在平台级管理功能中,并需注释说明原因。
|
||||
- 根据业务编码查找数据时,也必须受租户限制(编码只在租户内唯一)。
|
||||
- Queue Job 必须携带 `tenant_code`,执行时恢复 TenantContext。
|
||||
- Scheduler / Console Command 按租户遍历执行,不能一次性处理全部租户数据而不区分。
|
||||
- Webhook 根据店铺 / 应用凭证解析租户后再处理。
|
||||
- 缓存 Key、锁 Key 必须带 `tenant_code` 前缀。
|
||||
- 新增接口需有租户隔离测试。
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: 测试要求(后端 Pest/PHPUnit、Python pytest)
|
||||
globs: backend/tests/**,py-service/tests/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# 测试要求
|
||||
|
||||
详见 `docs/14-testing.md`。核心业务没有测试不继续堆功能。
|
||||
|
||||
## 必测
|
||||
Tenant Isolation、RBAC、Inventory、Concurrent Inventory、Inbound、Outbound、Allocation、Picking、Stocktake、Order Status、Queue、API Authorization。
|
||||
|
||||
## 关键用例
|
||||
- **库存并发**:库存 10,两个请求同时扣 8 → 只能成功一个,不出现 `available_qty = -6`。并发测试需使用真实 MySQL(不用 SQLite),多进程 / 多连接触发。
|
||||
- **租户隔离**:租户 A 通过列表、详情、业务编码、批量操作都无法访问租户 B 数据。
|
||||
- **状态机**:合法转换成功并记录日志;非法转换(如 `draft → shipped`)被拒绝。
|
||||
- **库存流水**:每次余额变化都有流水,流水累计与余额一致。
|
||||
- **授权**:无权限用户调用接口返回失败格式与正确状态码。
|
||||
|
||||
## 约定
|
||||
- Feature 测试覆盖 API(含返回格式),Unit 测试覆盖 Domain 规则与状态机。
|
||||
- 使用 Factory 构造数据,Factory 默认带 `tenant_code`。
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
description: WMS 领域规则:业务编码、状态机、统一订单模型、第三方平台 Connector
|
||||
globs: backend/app/Domain/**,backend/app/Application/**,backend/app/Infrastructure/**,backend/app/Models/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# WMS 领域规则
|
||||
|
||||
详见 `docs/04-domain-model.md`。
|
||||
|
||||
## 业务编码
|
||||
- 业务关联优先使用 `tenant_code`、`warehouse_code`、`location_code`、`customer_code`、`sku_code`、`order_no`、`inbound_no`、`outbound_no`、`picking_no`、`shipment_no`、`package_no`。
|
||||
- 单号生成集中在一个编号服务中(租户内唯一、并发安全),不要各模块各自拼接。
|
||||
|
||||
## 状态机
|
||||
- 状态定义为 Enum,合法转换集中声明,非法转换抛异常;状态变更写日志。
|
||||
- 出库:`draft → allocated → picking → picked → packing → packed → shipped → completed`
|
||||
|
||||
```php
|
||||
// ❌
|
||||
if ($outbound->status == 'packed') { $outbound->status = 'shipped'; }
|
||||
|
||||
// ✅
|
||||
$outbound->transitionTo(OutboundStatus::Shipped); // 内部校验 canTransitionTo,失败抛 InvalidStatusTransition
|
||||
```
|
||||
|
||||
## 统一订单模型
|
||||
- `order` / `order_item` / `order_source` / `order_channel` / `order_external_no` / `order_status`。
|
||||
- 核心订单表、Domain、Application 中**不得出现** Amazon / Shopify / TikTok 等平台特有字段与分支判断。
|
||||
|
||||
## 第三方平台
|
||||
- 实现 `MarketplaceConnector` 接口,放 `app/Infrastructure/Platforms/{Platform}/`。
|
||||
- Connector 负责平台数据 ↔ 统一模型(订单、SKU、库存、物流、状态)的映射。
|
||||
- 新增平台只新增 Connector 与配置,不修改核心 WMS 代码。
|
||||
- 平台调用走 Queue,不在用户 HTTP 请求中同步等待。
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
商业化、多租户的仓储管理系统(Warehouse Management System)。面向电商卖家、海外仓、第三方仓与 3PL,支持多租户、多仓库、多客户、多 SKU,以及入库、出库、库存与电商订单接入。
|
||||
|
||||
当前处于 **Phase 1:架构与数据模型设计**。设计文档在 [`docs/`](./docs/README.md),代码目录(`backend/`、`frontend/`、`py-service/`)尚未创建。开发顺序见 [`docs/16-roadmap.md`](./docs/16-roadmap.md)。
|
||||
当前处于 **Phase 2:`backend/` 已初始化**(统一响应、TenantContext、`GET /api/v1/health`)。业务表与前端尚未开始。开发顺序见 [`docs/16-roadmap.md`](./docs/16-roadmap.md)。
|
||||
|
||||
## 架构
|
||||
|
||||
@@ -42,7 +42,7 @@ API 前缀:`/api/v1/...`。
|
||||
|
||||
```
|
||||
wms-saas/
|
||||
├── backend/ Laravel 核心业务(待建)
|
||||
├── backend/ Laravel 核心业务(Phase 2 基础已在)
|
||||
├── frontend/ Vue 3 Admin(待建)
|
||||
├── py-service/ Python AI / Data Service(待建)
|
||||
├── docs/ 设计文档与表结构
|
||||
@@ -78,10 +78,11 @@ wms-saas/
|
||||
|
||||
## 本地开发
|
||||
|
||||
环境与业务代码尚未初始化。进入对应 Phase 后:
|
||||
`backend/` 已可运行测试。本机需 PHP 8.4;当前仓库还没有根目录 `docker-compose.yml`(Phase 17)。
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
cd backend
|
||||
php artisan test
|
||||
```
|
||||
|
||||
敏感配置通过 `.env` 注入,不提交到 Git。
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[{compose,docker-compose}.{yml,yaml}]
|
||||
indent_size = 4
|
||||
@@ -0,0 +1,65 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost:8000
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
@@ -0,0 +1,27 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.codex
|
||||
/.cursor/
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/fonts-manifest.dev.json
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
_ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,2 @@
|
||||
ignore-scripts=true
|
||||
audit=true
|
||||
@@ -0,0 +1,47 @@
|
||||
<laravel-boost-guidelines>
|
||||
# Laravel Application
|
||||
|
||||
This repository contains a Laravel application. Complete the following setup before working on the user's request.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Verify that PHP and Composer are available:
|
||||
|
||||
```sh
|
||||
php -v
|
||||
composer -V
|
||||
```
|
||||
|
||||
If either command is unavailable, detect the user's operating system and install the prerequisites with the appropriate command:
|
||||
|
||||
macOS:
|
||||
|
||||
```sh
|
||||
/bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.5)"
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.5'))
|
||||
```
|
||||
|
||||
Linux:
|
||||
|
||||
```sh
|
||||
/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.5)"
|
||||
```
|
||||
|
||||
After installation, ask the user to restart their terminal. If the agent needs the restarted shell to continue, ask the user to reopen their terminal and rerun their original prompt.
|
||||
|
||||
## Agent Setup
|
||||
|
||||
Install Laravel Boost from the application root before making application changes:
|
||||
|
||||
```sh
|
||||
composer require laravel/boost --dev
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
Boost replaces these bootstrap instructions with guidelines tailored to the application. After installation, read `AGENTS.md` again and continue with the user's original request using the generated guidelines.
|
||||
</laravel-boost-guidelines>
|
||||
@@ -0,0 +1,47 @@
|
||||
<laravel-boost-guidelines>
|
||||
# Laravel Application
|
||||
|
||||
This repository contains a Laravel application. Complete the following setup before working on the user's request.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Verify that PHP and Composer are available:
|
||||
|
||||
```sh
|
||||
php -v
|
||||
composer -V
|
||||
```
|
||||
|
||||
If either command is unavailable, detect the user's operating system and install the prerequisites with the appropriate command:
|
||||
|
||||
macOS:
|
||||
|
||||
```sh
|
||||
/bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.5)"
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.5'))
|
||||
```
|
||||
|
||||
Linux:
|
||||
|
||||
```sh
|
||||
/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.5)"
|
||||
```
|
||||
|
||||
After installation, ask the user to restart their terminal. If the agent needs the restarted shell to continue, ask the user to reopen their terminal and rerun their original prompt.
|
||||
|
||||
## Agent Setup
|
||||
|
||||
Install Laravel Boost from the application root before making application changes:
|
||||
|
||||
```sh
|
||||
composer require laravel/boost --dev
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
Boost replaces these bootstrap instructions with guidelines tailored to the application. After installation, read `AGENTS.md` again and continue with the user's original request using the generated guidelines.
|
||||
</laravel-boost-guidelines>
|
||||
@@ -0,0 +1,58 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
|
||||
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
|
||||
|
||||
## Agentic Development
|
||||
|
||||
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
|
||||
|
||||
```bash
|
||||
composer require laravel/boost --dev
|
||||
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
final class HealthController extends Controller
|
||||
{
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
return ApiResponse::success([
|
||||
'status' => 'ok',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class AssignRequestId
|
||||
{
|
||||
public const ATTRIBUTE = 'request_id';
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$incoming = $request->headers->get('X-Request-Id');
|
||||
$requestId = is_string($incoming) && preg_match('/^[A-Za-z0-9_-]{1,64}$/', $incoming) === 1
|
||||
? $incoming
|
||||
: (string) Str::uuid();
|
||||
|
||||
$request->attributes->set(self::ATTRIBUTE, $requestId);
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $next($request);
|
||||
$response->headers->set('X-Request-Id', $requestId);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Support\TenantContext;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* 业务模型使用此 Trait 后,查询自动限定当前租户,创建时写入 tenant_code。
|
||||
* withoutGlobalScope('tenant') 只允许出现在平台级管理中,并在调用处说明原因。
|
||||
*/
|
||||
trait BelongsToTenant
|
||||
{
|
||||
public static function bootBelongsToTenant(): void
|
||||
{
|
||||
static::addGlobalScope('tenant', function (Builder $builder): void {
|
||||
$builder->where(
|
||||
$builder->getModel()->qualifyColumn('tenant_code'),
|
||||
app(TenantContext::class)->code(),
|
||||
);
|
||||
});
|
||||
|
||||
static::creating(function (Model $model): void {
|
||||
$model->setAttribute('tenant_code', app(TenantContext::class)->code());
|
||||
});
|
||||
|
||||
static::updating(function (Model $model): void {
|
||||
if ($model->isDirty('tenant_code')) {
|
||||
$model->setAttribute('tenant_code', $model->getOriginal('tenant_code'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Support\TenantContext;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(TenantContext::class);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
RateLimiter::for('api', function (Request $request): Limit {
|
||||
$tenantCode = app(TenantContext::class)->get() ?? 'guest';
|
||||
|
||||
return Limit::perMinute(60)->by($tenantCode.'|'.($request->ip() ?? 'unknown'));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class ApiExceptionResponse
|
||||
{
|
||||
public static function normalize(Response $response, Request $request): Response
|
||||
{
|
||||
if (! $request->is('api/*')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status = $response->getStatusCode();
|
||||
$payload = self::payload($response);
|
||||
|
||||
if (($payload['success'] ?? null) === false && array_key_exists('message', $payload)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
if ($status === 422 && isset($payload['errors']) && is_array($payload['errors'])) {
|
||||
$errors = $payload['errors'];
|
||||
}
|
||||
|
||||
$fallback = is_string($payload['message'] ?? null) ? $payload['message'] : '';
|
||||
|
||||
return ApiResponse::failure(self::messageForStatus($status, $fallback), $errors, $status);
|
||||
}
|
||||
|
||||
public static function messageForStatus(int $status, string $fallback): string
|
||||
{
|
||||
return match ($status) {
|
||||
401 => '未认证',
|
||||
403 => '无权访问',
|
||||
404 => '资源不存在',
|
||||
405 => '方法不允许',
|
||||
419 => '页面已过期',
|
||||
422 => '参数错误',
|
||||
429 => '请求过于频繁',
|
||||
default => $status >= 500 ? '操作失败' : ($fallback !== '' ? $fallback : '操作失败'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function payload(Response $response): array
|
||||
{
|
||||
if (! $response instanceof JsonResponse) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = $response->getData(true);
|
||||
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use stdClass;
|
||||
|
||||
final class ApiResponse
|
||||
{
|
||||
public const DEFAULT_PER_PAGE = 20;
|
||||
|
||||
public const MAX_PER_PAGE = 100;
|
||||
|
||||
public static function success(mixed $data = null, string $message = 'OK', int $status = 200): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
], $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $errors
|
||||
*/
|
||||
public static function failure(string $message, array $errors = [], int $status = 400): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
'errors' => $errors === [] ? new stdClass : $errors,
|
||||
], $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LengthAwarePaginator<int, mixed> $paginator
|
||||
*/
|
||||
public static function paginated(LengthAwarePaginator $paginator, string $message = 'OK'): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => array_values($paginator->items()),
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function perPage(mixed $requested): int
|
||||
{
|
||||
if (! is_numeric($requested)) {
|
||||
return self::DEFAULT_PER_PAGE;
|
||||
}
|
||||
|
||||
$pageSize = (int) $requested;
|
||||
|
||||
if ($pageSize < 1) {
|
||||
return self::DEFAULT_PER_PAGE;
|
||||
}
|
||||
|
||||
return min($pageSize, self::MAX_PER_PAGE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class MissingTenantContextException extends RuntimeException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('当前没有租户上下文。');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
final class TenantContext
|
||||
{
|
||||
private ?string $tenantCode = null;
|
||||
|
||||
public function set(string $tenantCode): void
|
||||
{
|
||||
if ($tenantCode === '') {
|
||||
throw new \InvalidArgumentException('tenant_code 不能为空。');
|
||||
}
|
||||
|
||||
$this->tenantCode = $tenantCode;
|
||||
}
|
||||
|
||||
public function forget(): void
|
||||
{
|
||||
$this->tenantCode = null;
|
||||
}
|
||||
|
||||
public function has(): bool
|
||||
{
|
||||
return $this->tenantCode !== null;
|
||||
}
|
||||
|
||||
public function get(): ?string
|
||||
{
|
||||
return $this->tenantCode;
|
||||
}
|
||||
|
||||
public function code(): string
|
||||
{
|
||||
if ($this->tenantCode === null) {
|
||||
throw new MissingTenantContextException;
|
||||
}
|
||||
|
||||
return $this->tenantCode;
|
||||
}
|
||||
|
||||
public function cacheKey(string $key): string
|
||||
{
|
||||
return 'tenant:'.$this->code().':'.$key;
|
||||
}
|
||||
|
||||
public function lockKey(string $key): string
|
||||
{
|
||||
return 'lock:tenant:'.$this->code().':'.$key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @param callable(): T $callback
|
||||
* @return T
|
||||
*/
|
||||
public function run(string $tenantCode, callable $callback): mixed
|
||||
{
|
||||
$previous = $this->tenantCode;
|
||||
$this->set($tenantCode);
|
||||
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
$this->tenantCode = $previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\AssignRequestId;
|
||||
use App\Support\ApiExceptionResponse;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->throttleApi();
|
||||
$middleware->api(prepend: [
|
||||
AssignRequestId::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
|
||||
);
|
||||
|
||||
$exceptions->respond(function (Response $response, Throwable $exception, Request $request): Response {
|
||||
return ApiExceptionResponse::normalize($response, $request);
|
||||
});
|
||||
})->create();
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"laravel/framework": "^13.17",
|
||||
"laravel/tinker": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/pao": "^1.0.6",
|
||||
"laravel/pint": "^1.27",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^12.5.12"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install --ignore-scripts",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@php artisan dev"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi @no_additional_args",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
Generated
+8358
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "storage", "octane",
|
||||
| "session", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'storage' => [
|
||||
'driver' => 'storage',
|
||||
'disk' => env('CACHE_STORAGE_DISK'),
|
||||
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Serializable Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the classes that can be unserialized from cache
|
||||
| storage. By default, no PHP classes will be unserialized from your
|
||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||
|
|
||||
*/
|
||||
|
||||
'serializable_classes' => false,
|
||||
|
||||
];
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "monthly", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'max_files' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'monthly' => [
|
||||
'driver' => 'monthly',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'max_files' => 3,
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Resend, Postmark, AWS, and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain without subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Serialization
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the serialization strategy for session data, which
|
||||
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||
| objects in the session but can make an application vulnerable to
|
||||
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||
|
|
||||
| Supported: "json", "php"
|
||||
|
|
||||
*/
|
||||
|
||||
'serialization' => 'json',
|
||||
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedSmallInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->string('connection');
|
||||
$table->string('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
|
||||
$table->index(['connection', 'queue', 'failed_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"concurrently": "^10.0.3",
|
||||
"laravel-vite-plugin": "^3.1",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@laravel/multiplex": "^0.4.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_URL" value=""/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,9 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
//
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Http\Controllers\Api\V1\HealthController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('v1')->group(function (): void {
|
||||
Route::get('health', HealthController::class);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,10 @@
|
||||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
lsp-*.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ApiExceptionTest extends TestCase
|
||||
{
|
||||
public function test_unknown_api_route_uses_the_failure_envelope(): void
|
||||
{
|
||||
config(['app.debug' => true]);
|
||||
|
||||
$response = $this->getJson('/api/v1/does-not-exist');
|
||||
|
||||
$response->assertNotFound()
|
||||
->assertExactJson([
|
||||
'success' => false,
|
||||
'message' => '资源不存在',
|
||||
'errors' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_validation_errors_use_the_failure_envelope(): void
|
||||
{
|
||||
Route::post('/api/v1/_validation_probe', function (Request $request) {
|
||||
$request->validate(['name' => ['required', 'string']]);
|
||||
|
||||
return ApiResponse::success();
|
||||
});
|
||||
|
||||
$response = $this->postJson('/api/v1/_validation_probe', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('message', '参数错误')
|
||||
->assertJsonStructure(['errors' => ['name']]);
|
||||
}
|
||||
|
||||
public function test_method_not_allowed_uses_the_failure_envelope(): void
|
||||
{
|
||||
$this->postJson('/api/v1/health')
|
||||
->assertStatus(405)
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('message', '方法不允许');
|
||||
}
|
||||
|
||||
public function test_paginated_payload_uses_meta(): void
|
||||
{
|
||||
$paginator = new LengthAwarePaginator(
|
||||
items: [['sku_code' => 'A-1']],
|
||||
total: 21,
|
||||
perPage: 20,
|
||||
currentPage: 1,
|
||||
);
|
||||
|
||||
$payload = ApiResponse::paginated($paginator)->getData(true);
|
||||
|
||||
$this->assertSame([
|
||||
'success' => true,
|
||||
'message' => 'OK',
|
||||
'data' => [
|
||||
['sku_code' => 'A-1'],
|
||||
],
|
||||
'meta' => [
|
||||
'current_page' => 1,
|
||||
'per_page' => 20,
|
||||
'total' => 21,
|
||||
'last_page' => 2,
|
||||
],
|
||||
], $payload);
|
||||
}
|
||||
|
||||
public function test_per_page_is_capped(): void
|
||||
{
|
||||
$this->assertSame(20, ApiResponse::perPage(null));
|
||||
$this->assertSame(20, ApiResponse::perPage(0));
|
||||
$this->assertSame(50, ApiResponse::perPage('50'));
|
||||
$this->assertSame(100, ApiResponse::perPage(500));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class HealthTest extends TestCase
|
||||
{
|
||||
public function test_health_returns_the_standard_envelope(): void
|
||||
{
|
||||
$response = $this->getJson('/api/v1/health');
|
||||
|
||||
$response->assertOk()
|
||||
->assertExactJson([
|
||||
'success' => true,
|
||||
'message' => 'OK',
|
||||
'data' => [
|
||||
'status' => 'ok',
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertHeader('X-Request-Id');
|
||||
}
|
||||
|
||||
public function test_health_keeps_a_valid_request_id(): void
|
||||
{
|
||||
$response = $this->withHeader('X-Request-Id', 'req_123')
|
||||
->getJson('/api/v1/health');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertHeader('X-Request-Id', 'req_123');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Support\MissingTenantContextException;
|
||||
use App\Support\TenantContext;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\Fixtures\TenantProbe;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TenantScopeTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Schema::dropIfExists('tenant_probes');
|
||||
Schema::create('tenant_probes', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code', 32);
|
||||
$table->string('name');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Schema::dropIfExists('tenant_probes');
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_queries_only_return_the_current_tenant(): void
|
||||
{
|
||||
$context = app(TenantContext::class);
|
||||
|
||||
$context->set('tenant-a');
|
||||
TenantProbe::query()->create(['name' => 'alpha']);
|
||||
|
||||
$context->set('tenant-b');
|
||||
TenantProbe::query()->create(['name' => 'beta']);
|
||||
|
||||
$context->set('tenant-a');
|
||||
|
||||
$this->assertSame(['alpha'], TenantProbe::query()->pluck('name')->all());
|
||||
}
|
||||
|
||||
public function test_create_always_uses_the_current_tenant(): void
|
||||
{
|
||||
app(TenantContext::class)->set('tenant-a');
|
||||
|
||||
TenantProbe::query()->create([
|
||||
'name' => 'alpha',
|
||||
'tenant_code' => 'tenant-b',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('tenant_probes', [
|
||||
'name' => 'alpha',
|
||||
'tenant_code' => 'tenant-a',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_tenant_code_cannot_be_reassigned(): void
|
||||
{
|
||||
app(TenantContext::class)->set('tenant-a');
|
||||
|
||||
$row = TenantProbe::query()->create(['name' => 'alpha']);
|
||||
$row->tenant_code = 'tenant-b';
|
||||
$row->name = 'renamed';
|
||||
$row->save();
|
||||
|
||||
$this->assertDatabaseHas('tenant_probes', [
|
||||
'id' => $row->id,
|
||||
'tenant_code' => 'tenant-a',
|
||||
'name' => 'renamed',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_query_without_context_fails_closed(): void
|
||||
{
|
||||
$this->expectException(MissingTenantContextException::class);
|
||||
|
||||
TenantProbe::query()->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Fixtures;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantProbe extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'tenant_probes';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Support\MissingTenantContextException;
|
||||
use App\Support\TenantContext;
|
||||
use InvalidArgumentException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TenantContextTest extends TestCase
|
||||
{
|
||||
public function test_code_requires_a_tenant(): void
|
||||
{
|
||||
$this->expectException(MissingTenantContextException::class);
|
||||
|
||||
app(TenantContext::class)->code();
|
||||
}
|
||||
|
||||
public function test_empty_tenant_code_is_rejected(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
app(TenantContext::class)->set('');
|
||||
}
|
||||
|
||||
public function test_cache_and_lock_keys_include_the_tenant(): void
|
||||
{
|
||||
$context = app(TenantContext::class);
|
||||
$context->set('acme');
|
||||
|
||||
$this->assertSame('tenant:acme:sku:1', $context->cacheKey('sku:1'));
|
||||
$this->assertSame('lock:tenant:acme:inventory:1', $context->lockKey('inventory:1'));
|
||||
}
|
||||
|
||||
public function test_run_restores_the_previous_tenant(): void
|
||||
{
|
||||
$context = app(TenantContext::class);
|
||||
$context->set('tenant-a');
|
||||
|
||||
$seen = $context->run('tenant-b', fn (): string => $context->code());
|
||||
|
||||
$this->assertSame('tenant-b', $seen);
|
||||
$this->assertSame('tenant-a', $context->code());
|
||||
}
|
||||
|
||||
public function test_run_clears_context_when_none_was_set(): void
|
||||
{
|
||||
$context = app(TenantContext::class);
|
||||
|
||||
$context->run('tenant-b', fn (): string => $context->code());
|
||||
|
||||
$this->assertFalse($context->has());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import { bunny } from 'laravel-vite-plugin/fonts';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
refresh: true,
|
||||
fonts: [
|
||||
bunny('Instrument Sans', {
|
||||
weights: [400, 500, 600],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
},
|
||||
},
|
||||
});
|
||||
+19
-1
@@ -51,7 +51,25 @@ POST /api/v1/outbounds/{outbound_no}/ship
|
||||
}
|
||||
```
|
||||
|
||||
分页数据使用统一格式(具体字段在 Phase 2 确定并固化到统一响应工具类中),所有列表接口保持一致。
|
||||
分页列表的 `data` 是当前页记录,分页信息放在同级 `meta`。所有列表接口都走 `ApiResponse::paginated()`。
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "OK",
|
||||
"data": [],
|
||||
"meta": {
|
||||
"current_page": 1,
|
||||
"per_page": 20,
|
||||
"total": 0,
|
||||
"last_page": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
默认每页 20 条,最大 100 条。失败响应的 `errors` 没有内容时是 JSON 对象 `{}`,不是数组。
|
||||
|
||||
`/api/*` 的异常由统一异常处理转成失败格式,不返回框架堆栈。响应头 `X-Request-Id` 贯穿本次请求;客户端传入的值仅在字母、数字、下划线、连字符且不超过 64 位时沿用。
|
||||
|
||||
## 内部 API(Python Service)
|
||||
|
||||
|
||||
+1
-1
@@ -62,6 +62,6 @@
|
||||
|
||||
## 设计产出
|
||||
|
||||
表结构设计统一存放在 [`docs/database/`](./database/README.md)。
|
||||
表结构是 MySQL 建表语句,每张表一个文件,索引在 [`docs/database/README.md`](./database/README.md),约定在 [`docs/database/00-conventions.md`](./database/00-conventions.md)。
|
||||
|
||||
在写迁移之前,先在 `docs/database/` 中产出并确认表结构,把租户、仓库、库位、SKU、库存、库存流水、入库、出库这几个核心模型定死,再开始写 Laravel。迁移与设计文档必须保持一致。
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# 数据库约定
|
||||
|
||||
本目录的 `.sql` 是表结构的设计稿,供评审和后续 Laravel migration 对照。不要把这些文件当作生产迁移直接执行,也不要一次把尚未开工的 Phase 全部做成 migration。
|
||||
|
||||
## 库与字符集
|
||||
|
||||
- MySQL 8、InnoDB、`utf8mb4`、`utf8mb4_0900_ai_ci`
|
||||
- 时间用 `DATETIME(3)`,由应用写入租户或仓库时区换算后的时刻,库内不存时区
|
||||
|
||||
## 命名
|
||||
|
||||
- 表名使用单数,统一 `wms_` 前缀,例如 `wms_sku`、`wms_stock_log`
|
||||
- 每张表一个文件,文件名即表名,放在对应领域目录
|
||||
- 字段 snake_case。业务编码以 `_code` 或 `_no` 结尾
|
||||
|
||||
## 公共字段
|
||||
|
||||
- `id`:`BIGINT UNSIGNED` 自增,只做内部主键和加锁顺序
|
||||
- `tenant_code`:核心业务表必有。例外只有两张全局目录:`wms_tenant`、`wms_permission`
|
||||
- `created_at`、`updated_at`:每张表都有
|
||||
- 单据头有 `created_by`、`updated_by`,存 `user_code`
|
||||
- 不使用 `deleted_at`。主数据用 `status=disabled` 停用;流水和日志只追加,禁止修改和删除
|
||||
|
||||
## 类型
|
||||
|
||||
- 数量:`DECIMAL(16,4)`,禁止 `FLOAT` / `DOUBLE`
|
||||
- 金额:`DECIMAL(16,4)`,币种 `CHAR(3)`
|
||||
- 重量:千克,`DECIMAL(12,4)`;尺寸:厘米
|
||||
- 状态:`VARCHAR(32)`,取值写在字段注释里。应用层用 Enum 和集中定义的合法迁移,禁止散落比较魔法字符串
|
||||
|
||||
## 关联
|
||||
|
||||
- 表与表用业务编码关联,不建外键。编码的唯一约束都包含 `tenant_code`
|
||||
- 需要「允许多个空值」的可选单号(外部订单号、入库参考号、非销售出库的订单号)使用 `NULL`,不要用空字符串,否则唯一索引会撞车
|
||||
- 参与库存唯一键的可选编码(库位、批次)使用空字符串,保证唯一索引生效
|
||||
- Laravel 的 `personal_access_tokens`、队列表、缓存表由框架 migration 维护,不在这里重复
|
||||
|
||||
## 库存
|
||||
|
||||
余额行的粒度是:租户 + 仓库 + 货主 + 库位 + SKU + 批次。
|
||||
|
||||
库存状态不另做一行维度,由数量桶表达:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| available_qty | 可用 |
|
||||
| reserved_qty | 已分配、未拣货 |
|
||||
| picked_qty | 已拣货、未出库 |
|
||||
| hold_qty | 冻结 |
|
||||
| damaged_qty | 残损 |
|
||||
| in_transit_qty | 在途,只放在库位编码为空的行 |
|
||||
|
||||
待检、待上架的实物放在收货或质检库位,仍记在 `available_qty`,靠库位策略排除出可分配范围。
|
||||
|
||||
任何余额变化必须和 `wms_stock_log` 在同一事务。扣减用带数量条件的原子更新。一次动作跨两个桶或两个库位时写多行流水,共用 `biz_type` 和 `biz_no`。打包不改数量桶。
|
||||
|
||||
## 订单和出库
|
||||
|
||||
- `wms_order` 是统一履约订单,不包含任何平台专用列
|
||||
- `wms_outbound` 是仓库作业单。一笔订单可以按仓库拆成多张出库单
|
||||
- 平台差异留在 `wms_platform_shop` 和连接器
|
||||
|
||||
## 不单独建表的概念
|
||||
|
||||
| 概念 | 落点 |
|
||||
| --- | --- |
|
||||
| 库位容量 | `wms_location` 的容量列 |
|
||||
| 收货差异 | `wms_inbound_receipt_item` 的差异列 |
|
||||
| 入库流水 | `wms_stock_log`,`biz_type` 为入库相关 |
|
||||
| 库存锁定 / 预占 | `wms_allocation` |
|
||||
| 复核 | `wms_outbound.review_status` |
|
||||
| 报废单 | 退货明细的处置加 `wms_stock_log` |
|
||||
| 订单渠道、来源、外部单号、状态 | `wms_order` 的列,不是独立表 |
|
||||
| 报表、AI 分析结果 | 查询或 Python 服务返回,不落核心业务表 |
|
||||
@@ -0,0 +1,100 @@
|
||||
# ER 关系
|
||||
|
||||
Phase 1 锁定的核心模型。表结构见同目录各领域文件。退货、物流主数据、平台店铺、登录 / API 日志不在本图中,留到对应 Phase。
|
||||
|
||||
关联一律用业务编码,并始终带 `tenant_code`。
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
wms_tenant ||--o{ wms_user : tenant_code
|
||||
wms_tenant ||--o{ wms_role : tenant_code
|
||||
wms_role ||--o{ wms_user_role : role_code
|
||||
wms_user ||--o{ wms_user_role : user_code
|
||||
wms_role ||--o{ wms_role_permission : role_code
|
||||
wms_permission ||--o{ wms_role_permission : permission_code
|
||||
|
||||
wms_warehouse ||--o{ wms_zone : warehouse_code
|
||||
wms_zone ||--o{ wms_location : zone_code
|
||||
|
||||
wms_customer ||--o{ wms_product : customer_code
|
||||
wms_product ||--o{ wms_sku : product_code
|
||||
wms_sku ||--o{ wms_sku_barcode : sku_code
|
||||
wms_sku ||--o{ wms_pack_spec : sku_code
|
||||
|
||||
wms_sku ||--o{ wms_inventory : sku_code
|
||||
wms_location ||--o{ wms_inventory : location_code
|
||||
wms_batch ||--o{ wms_inventory : batch_no
|
||||
wms_inventory ||--o{ wms_stock_log : movement
|
||||
|
||||
wms_inbound ||--o{ wms_inbound_line : inbound_no
|
||||
wms_inbound_line ||--o{ wms_inbound_receipt : line_no
|
||||
wms_inbound_line ||--o{ wms_inbound_putaway : line_no
|
||||
|
||||
wms_order ||--o{ wms_order_line : order_no
|
||||
wms_order ||--o{ wms_outbound : order_no
|
||||
wms_outbound ||--o{ wms_outbound_line : outbound_no
|
||||
wms_outbound_line ||--o{ wms_outbound_allocation : line_no
|
||||
wms_wave ||--o{ wms_wave_outbound : wave_no
|
||||
wms_outbound ||--o{ wms_wave_outbound : outbound_no
|
||||
wms_outbound_allocation ||--o{ wms_pick_task : allocation_no
|
||||
wms_outbound ||--o{ wms_package : outbound_no
|
||||
wms_package ||--o{ wms_package_line : package_no
|
||||
wms_outbound ||--o{ wms_shipment : outbound_no
|
||||
```
|
||||
|
||||
## 库存落在哪一行
|
||||
|
||||
余额行的粒度:
|
||||
|
||||
`tenant_code + customer_code + warehouse_code + location_code + sku_code + batch_no`
|
||||
|
||||
货主(`customer_code`)必须在粒度里。租户是仓配公司,客户是 3PL 的货主,两家客户的库存不能混在一行。
|
||||
|
||||
在途、尚未落到库位时,`location_code` 为空字符串,表示虚拟在途位置。
|
||||
|
||||
## 数量怎么变
|
||||
|
||||
在手库存(不含在途):
|
||||
|
||||
`available_qty + reserved_qty + picked_qty + hold_qty + damaged_qty`
|
||||
|
||||
| 业务 | 数量桶 |
|
||||
| --- | --- |
|
||||
| 入库确认 | 在途 `+` |
|
||||
| 收货到收货位 | 在途 `-`,收货位可用或残损 `+` |
|
||||
| 上架 | 源库位可用 `-`,目标库位可用 `+` |
|
||||
| 分配 | 可用 `-`,预占 `+` |
|
||||
| 取消分配 | 预占 `-`,可用 `+` |
|
||||
| 拣货 | 预占 `-`,已拣 `+` |
|
||||
| 发货 | 已拣 `-`(离开仓库) |
|
||||
| 冻结 / 解冻 | 可用与冻结对转 |
|
||||
| 转残 / 转良 | 可用与残损对转 |
|
||||
| 调整、盘点确认 | 对应桶按差额 `+/-` |
|
||||
|
||||
每一次桶的变化写一行 `wms_stock_log`。同一次业务动作的多行共用 `movement_no`。余额更新与流水插入在同一数据库事务中。
|
||||
|
||||
## 单据链
|
||||
|
||||
```
|
||||
订单 wms_order
|
||||
→ 出库单 wms_outbound
|
||||
→ 分配 wms_outbound_allocation
|
||||
→ 波次 wms_wave(可无,则按单拣货)
|
||||
→ 拣货任务 wms_pick_task
|
||||
→ 包裹 wms_package
|
||||
→ 发货 wms_shipment
|
||||
```
|
||||
|
||||
第一阶段业务上一单一张出库单。表结构允许多张出库单指向同一 `order_no`,后续拆单不用改表。
|
||||
|
||||
入库不经过订单:
|
||||
|
||||
```
|
||||
入库单 → 明细 → 收货记录 → 上架记录 → 库存余额 + 流水
|
||||
```
|
||||
|
||||
收货差异不单独建表,用明细上的 `expected_qty - received_qty` 得出。
|
||||
|
||||
## 状态日志
|
||||
|
||||
入库、出库、订单、波次、调整、盘点的状态变化写 `wms_status_log`(见 [10-saas.md](./10-saas.md))。非法转换在领域层拒绝,不写日志。
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 表:wms_number_sequence
|
||||
-- 用途:单号序列。按租户、业务类型发号,保证并发下单号不重复。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:发号必须用单条原子更新:UPDATE ... SET current_value = LAST_INSERT_ID(current_value + 1),再 SELECT LAST_INSERT_ID()。禁止先查后加。
|
||||
|
||||
CREATE TABLE `wms_number_sequence` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码',
|
||||
`biz_type` VARCHAR(32) NOT NULL COMMENT '业务类型,如 order、inbound、outbound、picking',
|
||||
`period_key` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '重置周期,如 202609;空表示连续递增',
|
||||
`current_value` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前已分配到的序号',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_biz_period` (`tenant_code`, `biz_type`, `period_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='单号序列';
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 表:wms_permission
|
||||
-- 用途:权限。全局权限码目录,不按租户复制。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:本表无 tenant_code。权限码格式为 模块:动作,与前端按钮权限、后端 Policy 使用同一套编码。
|
||||
|
||||
CREATE TABLE `wms_permission` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`permission_code` VARCHAR(64) NOT NULL COMMENT '权限码,如 inventory:adjust',
|
||||
`permission_name` VARCHAR(64) NOT NULL COMMENT '权限名称',
|
||||
`module` VARCHAR(32) NOT NULL COMMENT '模块,如 inventory、inbound、outbound',
|
||||
`perm_type` VARCHAR(16) NOT NULL COMMENT 'menu 菜单,page 页面,button 按钮或接口',
|
||||
`parent_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '上级权限码,空表示顶级',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_permission_code` (`permission_code`),
|
||||
KEY `idx_module` (`module`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='权限';
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 表:wms_role
|
||||
-- 用途:角色。租户内角色。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:权限码本身是全局目录,角色与权限的绑定按租户隔离,见 wms_role_permission。
|
||||
|
||||
CREATE TABLE `wms_role` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码',
|
||||
`role_code` VARCHAR(32) NOT NULL COMMENT '角色编码,租户内唯一',
|
||||
`role_name` VARCHAR(64) NOT NULL COMMENT '角色名称',
|
||||
`status` VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT 'active 启用,disabled 停用',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_role_code` (`tenant_code`, `role_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色';
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 表:wms_role_permission
|
||||
-- 用途:角色权限。租户内角色与全局权限码的绑定。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:同一权限码可授予多个角色。解除授权删除本表行,不使用软删除。
|
||||
|
||||
CREATE TABLE `wms_role_permission` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码',
|
||||
`role_code` VARCHAR(32) NOT NULL COMMENT '角色编码',
|
||||
`permission_code` VARCHAR(64) NOT NULL COMMENT '权限码',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_role_perm` (`tenant_code`, `role_code`, `permission_code`),
|
||||
KEY `idx_tenant_perm` (`tenant_code`, `permission_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限';
|
||||
@@ -0,0 +1,21 @@
|
||||
-- 表:wms_tenant
|
||||
-- 用途:租户。SaaS 隔离边界,tenant_code 全局唯一。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:初始化时写入 tenant_code=platform 的平台租户,平台管理员挂在该租户下,不另建认证中心。本表没有 tenant_code 过滤列,因为行本身就是租户。
|
||||
|
||||
CREATE TABLE `wms_tenant` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码,全局唯一',
|
||||
`tenant_name` VARCHAR(128) NOT NULL COMMENT '租户名称',
|
||||
`status` VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT 'active 启用,suspended 停用',
|
||||
`timezone` VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai' COMMENT 'IANA 时区',
|
||||
`contact_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '联系人',
|
||||
`contact_phone` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '联系电话',
|
||||
`contact_email` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '联系邮箱',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_code` (`tenant_code`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='租户';
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 表:wms_tenant_setting
|
||||
-- 用途:租户设置。租户级可配置项,包括定时任务开关。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:密钥类配置不要放这里,第三方凭证放 wms_platform_shop.credential_cipher 并加密。
|
||||
|
||||
CREATE TABLE `wms_tenant_setting` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码',
|
||||
`setting_key` VARCHAR(64) NOT NULL COMMENT '配置键',
|
||||
`setting_value` TEXT NOT NULL COMMENT '配置值',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_setting_key` (`tenant_code`, `setting_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='租户设置';
|
||||
@@ -0,0 +1,22 @@
|
||||
-- 表:wms_user
|
||||
-- 用途:用户。租户内登录账号。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:令牌表使用 Laravel Sanctum 的 personal_access_tokens,不在本目录重复定义。
|
||||
|
||||
CREATE TABLE `wms_user` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码',
|
||||
`user_code` VARCHAR(64) NOT NULL COMMENT '用户编码,租户内唯一',
|
||||
`user_name` VARCHAR(64) NOT NULL COMMENT '姓名',
|
||||
`email` VARCHAR(128) NOT NULL COMMENT '登录邮箱,租户内唯一',
|
||||
`password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希,禁止保存明文',
|
||||
`status` VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT 'active 启用,disabled 停用,locked 锁定',
|
||||
`is_platform_admin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 表示平台管理员,仅 platform 租户使用',
|
||||
`last_login_at` DATETIME(3) NULL COMMENT '最近登录时间',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_user_code` (`tenant_code`, `user_code`),
|
||||
UNIQUE KEY `uk_tenant_email` (`tenant_code`, `email`),
|
||||
KEY `idx_tenant_status` (`tenant_code`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户';
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 表:wms_user_role
|
||||
-- 用途:用户角色。租户内用户与角色的绑定。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:一个用户可绑定多个角色。
|
||||
|
||||
CREATE TABLE `wms_user_role` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码',
|
||||
`user_code` VARCHAR(64) NOT NULL COMMENT '用户编码',
|
||||
`role_code` VARCHAR(32) NOT NULL COMMENT '角色编码',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_user_role` (`tenant_code`, `user_code`, `role_code`),
|
||||
KEY `idx_tenant_role` (`tenant_code`, `role_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色';
|
||||
@@ -0,0 +1,32 @@
|
||||
-- 表:wms_location
|
||||
-- 用途:库位。仓库内可存放库存的最小位置,同时保存容量。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:空置还是占用由库存余额推导,不在本表存占用状态。容量字段即库位容量,不另建容量表。
|
||||
|
||||
CREATE TABLE `wms_location` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码',
|
||||
`warehouse_code` VARCHAR(32) NOT NULL COMMENT '仓库编码',
|
||||
`zone_code` VARCHAR(32) NOT NULL COMMENT '库区编码',
|
||||
`location_code` VARCHAR(64) NOT NULL COMMENT '库位编码,仓库内唯一',
|
||||
`location_type_code` VARCHAR(32) NOT NULL COMMENT '库位类型编码',
|
||||
`aisle` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '巷道',
|
||||
`rack` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '货架',
|
||||
`level_no` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '层',
|
||||
`bin_no` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '位',
|
||||
`pick_sequence` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '拣货路径序号,小的优先',
|
||||
`status` VARCHAR(32) NOT NULL DEFAULT 'enabled' COMMENT 'enabled 可作业,disabled 停用,locked 锁定(盘点或异常,暂停入出)',
|
||||
`max_qty` DECIMAL(16,4) NULL COMMENT '容量:最大件数,空表示不限制',
|
||||
`max_weight` DECIMAL(12,4) NULL COMMENT '容量:最大重量千克,空表示不限制',
|
||||
`max_volume` DECIMAL(16,4) NULL COMMENT '容量:最大体积立方厘米,空表示不限制',
|
||||
`length_cm` DECIMAL(12,4) NULL COMMENT '长,厘米,空表示未维护',
|
||||
`width_cm` DECIMAL(12,4) NULL COMMENT '宽,厘米,空表示未维护',
|
||||
`height_cm` DECIMAL(12,4) NULL COMMENT '高,厘米,空表示未维护',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_wh_location` (`tenant_code`, `warehouse_code`, `location_code`),
|
||||
KEY `idx_tenant_wh_zone` (`tenant_code`, `warehouse_code`, `zone_code`),
|
||||
KEY `idx_tenant_wh_status` (`tenant_code`, `warehouse_code`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='库位';
|
||||
@@ -0,0 +1,26 @@
|
||||
-- 表:wms_location_strategy
|
||||
-- 用途:库位策略。上架、分配、拣货的规则。空编码表示该条件不限。
|
||||
-- 约定:InnoDB / utf8mb4 / 无外键,表间按业务编码关联。详见 docs/database/00-conventions.md
|
||||
-- 说明:新增平台或货主只加策略行,不改库存核心代码。
|
||||
|
||||
CREATE TABLE `wms_location_strategy` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
|
||||
`tenant_code` VARCHAR(32) NOT NULL COMMENT '租户编码',
|
||||
`strategy_code` VARCHAR(32) NOT NULL COMMENT '策略编码,租户内唯一',
|
||||
`strategy_name` VARCHAR(64) NOT NULL COMMENT '策略名称',
|
||||
`warehouse_code` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '仓库编码,空表示该租户全部仓库',
|
||||
`strategy_type` VARCHAR(32) NOT NULL COMMENT 'putaway 上架,allocation 库存分配,picking 拣货路径',
|
||||
`priority` INT UNSIGNED NOT NULL DEFAULT 100 COMMENT '优先级,数字小的先匹配',
|
||||
`customer_code` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '货主编码,空表示不限',
|
||||
`sku_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'SKU 编码,空表示不限',
|
||||
`zone_code` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '目标或来源库区,空表示不限',
|
||||
`location_type_code` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '库位类型,空表示不限',
|
||||
`rule_json` JSON NULL COMMENT '扩展条件,核心条件优先用独立列',
|
||||
`status` VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT 'active 启用,disabled 停用',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_strategy_code` (`tenant_code`, `strategy_code`),
|
||||
KEY `idx_tenant_wh_type` (`tenant_code`, `warehouse_code`, `strategy_type`, `priority`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='库位策略';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user