回筐、账单完成

This commit is contained in:
liu
2026-08-14 01:19:47 +08:00
parent e35c951a59
commit bf8ac68391
39 changed files with 1392 additions and 963 deletions
@@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Models\BillModel;
use App\Services\BillDetailService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
/**
* 小程序门店账单(采购单完成后由后台生成,门店端只读)
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class BillController extends BaseMiniController
{
/** 账单列表:当前门店强制过滤,?page=&pageSize= */
#[GetRoute('/bill', authorize: true)]
public function index(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$data = BillModel::query()
->where('store_id', $store->id)
->with('purchase:id,purchase_no,purchase_date')
->orderBy('bill_date', 'desc')
->orderBy('id', 'desc')
->paginate((int) $request->input('pageSize', 10))
->toArray();
return $this->success($data);
}
/** 账单详情(校验归属:仅能查看本店账单;含合并后的商品明细与关联订单) */
#[GetRoute('/bill/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$bill = BillModel::with('purchase:id,purchase_no,purchase_date')
->where('store_id', $store->id)
->find($id);
if ($bill === null) {
throw new RepositoryException('账单不存在');
}
$orders = $bill->orders()
->orderBy('id')
->get(['id', 'order_no', 'order_date', 'total_quantity', 'total_weight', 'total_amount', 'status'])
->toArray();
return $this->success([
'bill' => $bill->toArray(),
'items' => app(BillDetailService::class)->mergedItems($bill),
'orders' => $orders,
]);
}
/**
* 在线支付(预留接口,本次不实现;当前为线下收款,由后台手动登记)
*/
#[PostRoute('/bill/{id}/pay', authorize: true, where: ['id' => '[0-9]+'])]
public function pay(int $id, Request $request): JsonResponse
{
throw new RepositoryException('在线支付暂未开通,请线下付款后由商家登记收款');
}
}
@@ -108,7 +108,6 @@ class OrderController extends BaseMiniController
'order_date' => $now->toDateString(),
'total_quantity' => $totalQuantity,
'total_weight' => 0,
'product_amount' => $totalAmount,
'total_amount' => $totalAmount,
'status' => StoreOrderModel::STATUS_PENDING,
'remark' => $remark,
@@ -1,86 +0,0 @@
<?php
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Models\StatementModel;
use App\Services\ExportService;
use App\Services\StatementGenerateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Symfony\Component\HttpFoundation\Response;
/**
* 小程序门店对账单(自助生成 / 查看 / 导出)
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class StatementController extends BaseMiniController
{
/** 对账单列表(当前门店) */
#[GetRoute('/statement', authorize: true)]
public function index(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$data = StatementModel::query()
->where('store_id', $store->id)
->orderBy('id', 'desc')
->paginate((int) $request->input('pageSize', 10))
->toArray();
return $this->success($data);
}
/** 生成对账单:快照当前回款周期,settlement_date = period_end + cycle 天 */
#[PostRoute('/statement/generate', authorize: true)]
public function generate(Request $request): JsonResponse
{
$data = $request->validate([
'period_start' => 'required|date_format:Y-m-d',
'period_end' => 'required|date_format:Y-m-d|after_or_equal:period_start',
], [
'period_start.required' => '请选择对账周期开始日期',
'period_start.date_format' => '开始日期格式为 Y-m-d',
'period_end.required' => '请选择对账周期结束日期',
'period_end.date_format' => '结束日期格式为 Y-m-d',
'period_end.after_or_equal' => '结束日期不能早于开始日期',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$statement = app(StatementGenerateService::class)->generate(
$store,
$data['period_start'],
$data['period_end'],
);
return $this->success([
'id' => $statement->id,
'statement_no' => $statement->statement_no,
'total_amount' => $statement->total_amount,
'settlement_date' => $statement->settlement_date?->toDateString(),
], '对账单已生成');
}
/** 对账单详情(校验归属,含单品对账状态标识) */
#[GetRoute('/statement/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$statement = StatementModel::with('items')
->where('store_id', $store->id)
->find($id);
if ($statement === null) {
throw new RepositoryException('对账单不存在');
}
return $this->success($statement->toArray());
}
}
@@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\Recon;
use App\Exceptions\RepositoryException;
use App\Models\BillModel;
use App\Services\BillDetailService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 门店账单管理(采购单完成后按门店生成,后台查看 + 线下收款登记)
*/
#[RequestAttribute('/recon/bill', 'recon.bill')]
class BillController extends BaseController
{
protected array $searchField = [
'store_id' => '=',
'status' => '=',
'bill_no' => 'like',
'bill_date' => 'betweenDate',
];
/** 账单列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$query = BillModel::query()->with([
'store:id,name',
'purchase:id,purchase_no,purchase_date',
'operator:id,nickname',
])->withCount('orders');
// 按采购单号搜索
$purchaseNo = trim((string) ($params['purchase_no'] ?? ''));
if ($purchaseNo !== '') {
$keyword = '%' . str_replace('%', '\%', $purchaseNo) . '%';
$query->whereHas('purchase', static function ($purchaseQuery) use ($keyword) {
$purchaseQuery->where('purchase_no', 'like', $keyword);
});
}
$data = $this->buildSearch($params, $query)
->orderBy('bill_date', 'desc')
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
return $this->success($data);
}
/**
* 账单详情:账单信息 + 合并后的商品明细(按商品聚合)+ 关联订单
*/
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
{
$bill = BillModel::with([
'store:id,name,address,contact,phone',
'purchase:id,purchase_no,purchase_date,status',
'operator:id,nickname',
'paidOperator:id,nickname',
])->find($id);
if (empty($bill)) {
throw new RepositoryException('账单不存在');
}
$orders = $bill->orders()
->orderBy('id')
->get(['id', 'order_no', 'order_date', 'total_quantity', 'total_weight', 'total_amount', 'status'])
->toArray();
return $this->success([
'bill' => $bill->toArray(),
'items' => app(BillDetailService::class)->mergedItems($bill),
'orders' => $orders,
]);
}
/**
* 确认收款:线下收款后手动登记付款信息,支付状态置为已支付
*/
#[PutRoute(route: '/{id}/pay', authorize: 'pay', where: ['id' => '[0-9]+'])]
public function pay(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'paid_at' => 'sometimes|date_format:Y-m-d H:i:s',
'pay_remark' => 'nullable|string|max:255',
], [
'paid_at.date_format' => '付款时间格式为 Y-m-d H:i:s',
'pay_remark.max' => '付款备注超过最大长度',
]);
$bill = BillModel::find($id);
if (empty($bill)) {
throw new RepositoryException('账单不存在');
}
if ($bill->status === BillModel::STATUS_PAID) {
throw new RepositoryException('账单已支付,请勿重复收款');
}
$bill->status = BillModel::STATUS_PAID;
$bill->paid_at = $data['paid_at'] ?? now();
$bill->pay_remark = (string) ($data['pay_remark'] ?? '');
$bill->paid_operator_id = (int) $request->user()->id;
$bill->save();
return $this->success([], '收款已登记');
}
}
@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\Recon;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Recon\ContainerReturnFormRequest;
use App\Models\ContainerReturnModel;
use App\Models\StoreModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\AnnoRoute\Attribute\DeleteRoute;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
use Throwable;
/**
* 回筐记录(压筐=生成账单自动写入只读;回筐=门店退回手动登记,扣减门店待回数量)
*/
#[RequestAttribute('/recon/container-return', 'recon.containerReturn')]
class ContainerReturnController extends BaseController
{
protected array $searchField = [
'store_id' => '=',
'type' => '=',
'return_date' => 'betweenDate',
];
/** 回筐记录列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch($params, ContainerReturnModel::query()->with([
'store:id,name,pending_box_num,pending_tray_num',
'bill:id,bill_no',
'operator:id,nickname',
]))
->orderBy('return_date', 'desc')
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
return $this->success($data);
}
/**
* 回筐登记:门店退回周转筐/托盘,扣减门店待回数量(超回拒绝)
* @throws Throwable
*/
#[PostRoute(authorize: 'create')]
public function create(ContainerReturnFormRequest $request): JsonResponse
{
$data = $request->validated();
$boxNum = (int) $data['box_num'];
$trayNum = (int) $data['tray_num'];
if ($boxNum === 0 && $trayNum === 0) {
throw new RepositoryException('周转筐与周转托盘数量不能同时为 0');
}
return DB::transaction(function () use ($data, $boxNum, $trayNum, $request) {
$store = StoreModel::query()->lockForUpdate()->find((int) $data['store_id']);
if (empty($store)) {
throw new RepositoryException('门店不存在');
}
if ($boxNum > (int) $store->pending_box_num) {
throw new RepositoryException(
'回筐数量超过该门店待回筐数量(当前待回 ' . (int) $store->pending_box_num . ' 个)'
);
}
if ($trayNum > (int) $store->pending_tray_num) {
throw new RepositoryException(
'回托盘数量超过该门店待回托盘数量(当前待回 ' . (int) $store->pending_tray_num . ' 个)'
);
}
$store->pending_box_num = (int) $store->pending_box_num - $boxNum;
$store->pending_tray_num = (int) $store->pending_tray_num - $trayNum;
$store->save();
ContainerReturnModel::create([
'store_id' => $store->id,
'bill_id' => 0,
'type' => ContainerReturnModel::TYPE_RETURN,
'box_num' => $boxNum,
'tray_num' => $trayNum,
'return_date' => $data['return_date'],
'operator_id' => (int) $request->user()->id,
'remark' => (string) ($data['remark'] ?? ''),
]);
return $this->success([], '回筐已登记');
});
}
/**
* 删除回筐记录(仅手动登记的回筐记录可删,删除后恢复门店待回数量;压筐记录随账单生成不允许删除)
* @throws Throwable
*/
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
public function delete(int $id): JsonResponse
{
$record = ContainerReturnModel::find($id);
if (empty($record)) {
throw new RepositoryException('回筐记录不存在');
}
if ($record->type !== ContainerReturnModel::TYPE_RETURN) {
throw new RepositoryException('压筐记录由账单生成,不允许删除');
}
return DB::transaction(function () use ($record) {
$store = StoreModel::query()->lockForUpdate()->find($record->store_id);
if ($store !== null) {
$store->pending_box_num = (int) $store->pending_box_num + (int) $record->box_num;
$store->pending_tray_num = (int) $store->pending_tray_num + (int) $record->tray_num;
$store->save();
}
$record->delete();
return $this->success([], '回筐记录已删除,门店待回数量已恢复');
});
}
}
@@ -1,49 +0,0 @@
<?php
namespace App\Http\Controllers\Recon;
use App\Exceptions\RepositoryException;
use App\Models\StatementModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 门店对账单管理(后台只读视角;生成/导出在小程序端)
*/
#[RequestAttribute('/recon/statement', 'recon.statement')]
class StatementController extends BaseController
{
protected array $searchField = [
'statement_no' => 'like',
'store_id' => '=',
'status' => '=',
'period_start' => 'betweenDate',
];
/** 对账单列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch($params, StatementModel::query()->with('store:id,name'))
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
return $this->success($data);
}
/** 对账单详情(含明细) */
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
{
$statement = StatementModel::with(['store:id,name', 'items'])->find($id);
if (empty($statement)) {
throw new RepositoryException('对账单不存在');
}
return $this->success($statement->toArray());
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Requests\Recon;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 回筐登记 验证(门店退回周转筐/托盘,扣减门店待回数量)
*/
class ContainerReturnFormRequest extends BaseFormRequest
{
protected $stopOnFirstFailure = true;
public function rules(): array
{
return [
'store_id' => 'required|integer|exists:store,id',
'box_num' => 'required|integer|min:0',
'tray_num' => 'required|integer|min:0',
'return_date' => 'required|date_format:Y-m-d',
'remark' => 'nullable|string|max:255',
];
}
public function messages(): array
{
return [
'store_id.required' => '请选择门店',
'store_id.exists' => '门店不存在',
'box_num.required' => '周转筐数量不能为空',
'box_num.integer' => '周转筐数量必须为整数',
'box_num.min' => '周转筐数量不能小于 0',
'tray_num.required' => '周转托盘数量不能为空',
'tray_num.integer' => '周转托盘数量必须为整数',
'tray_num.min' => '周转托盘数量不能小于 0',
'return_date.required' => '请选择退回日期',
'return_date.date_format' => '退回日期格式为 Y-m-d',
'remark.max' => '备注超过最大长度',
];
}
}