订单支付记录
This commit is contained in:
@@ -8,7 +8,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -17,17 +16,21 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
|
||||
class BillController extends BaseMiniController
|
||||
{
|
||||
/** 账单列表:当前门店强制过滤,?page=&pageSize= */
|
||||
/** 账单列表:当前门店强制过滤,?status= 按支付状态筛选(0未支付 1已支付) */
|
||||
#[GetRoute('/bill', authorize: true)]
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->currentUser($request);
|
||||
$store = $this->ensureStoreBound($user);
|
||||
|
||||
$data = BillModel::query()
|
||||
$query = BillModel::query()
|
||||
->where('store_id', $store->id)
|
||||
->with('purchase:id,purchase_no,purchase_date')
|
||||
->orderBy('bill_date', 'desc')
|
||||
->with('purchase:id,purchase_no,purchase_date');
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', (int) $request->input('status'));
|
||||
}
|
||||
|
||||
$data = $query->orderBy('bill_date', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate((int) $request->input('pageSize', 10))
|
||||
->toArray();
|
||||
@@ -60,13 +63,4 @@ class BillController extends BaseMiniController
|
||||
'orders' => $orders,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线支付(预留接口,本次不实现;当前为线下收款,由后台手动登记)
|
||||
*/
|
||||
#[PostRoute('/bill/{id}/pay', authorize: true, where: ['id' => '[0-9]+'])]
|
||||
public function pay(int $id, Request $request): JsonResponse
|
||||
{
|
||||
throw new RepositoryException('在线支付暂未开通,请线下付款后由商家登记收款');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Mini;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Models\BillModel;
|
||||
use App\Models\PaymentModel;
|
||||
use App\Services\BillNumberService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 小程序门店支付(选择本店账单合并付款,提交汇款凭证,后台审核通过后账单置已支付)
|
||||
*/
|
||||
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
|
||||
class PaymentController extends BaseMiniController
|
||||
{
|
||||
/** 支付配置:收款码图片与对公汇款信息(付款页展示) */
|
||||
#[GetRoute('/payment/config', authorize: true)]
|
||||
public function config(): JsonResponse
|
||||
{
|
||||
// 配置值支持图片URL或文件ID(文件ID解析为预览地址)
|
||||
$resolve = static function (mixed $value): string {
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
if (is_numeric($value)) {
|
||||
return (string) (SysFileModel::query()->find((int) $value)?->preview_url ?? '');
|
||||
}
|
||||
return $value;
|
||||
};
|
||||
|
||||
return $this->success([
|
||||
'wechat_qrcode' => $resolve(site_config('pay.wechat_qrcode', '')),
|
||||
'alipay_qrcode' => $resolve(site_config('pay.alipay_qrcode', '')),
|
||||
'bank_info' => (string) site_config('pay.bank_info', ''),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 支付记录列表:当前门店强制过滤,?status=&page=&pageSize= */
|
||||
#[GetRoute('/payment', authorize: true)]
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->currentUser($request);
|
||||
$store = $this->ensureStoreBound($user);
|
||||
|
||||
$query = PaymentModel::query()
|
||||
->where('store_id', $store->id)
|
||||
->withCount('bills');
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', (int) $request->input('status'));
|
||||
}
|
||||
|
||||
$data = $query->orderBy('id', 'desc')
|
||||
->paginate((int) $request->input('pageSize', 10))
|
||||
->toArray();
|
||||
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起付款:合并选择本店未支付账单,提交支付方式与汇款凭证(后台审核)
|
||||
* @throws Throwable
|
||||
*/
|
||||
#[PostRoute('/payment', authorize: true)]
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'bill_ids' => 'required|array|min:1',
|
||||
'bill_ids.*' => 'integer|distinct',
|
||||
'pay_method' => 'required|integer|in:1,2,3',
|
||||
'voucher_ids' => 'required|array|min:1',
|
||||
'voucher_ids.*' => 'integer|distinct',
|
||||
'remark' => 'nullable|string|max:255',
|
||||
], [
|
||||
'bill_ids.required' => '请选择要付款的账单',
|
||||
'bill_ids.min' => '请选择要付款的账单',
|
||||
'pay_method.required' => '请选择支付方式',
|
||||
'pay_method.in' => '支付方式不正确',
|
||||
'voucher_ids.required' => '请上传汇款凭证',
|
||||
'voucher_ids.min' => '请上传汇款凭证',
|
||||
'remark.max' => '备注超过最大长度',
|
||||
]);
|
||||
|
||||
$user = $this->currentUser($request);
|
||||
$store = $this->ensureStoreBound($user);
|
||||
$billIds = array_map('intval', $data['bill_ids']);
|
||||
|
||||
$payment = DB::transaction(function () use ($store, $user, $data, $billIds) {
|
||||
$bills = BillModel::query()
|
||||
->where('store_id', $store->id)
|
||||
->whereIn('id', $billIds)
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
if ($bills->count() !== count($billIds)) {
|
||||
throw new RepositoryException('包含不属于本店的账单,请刷新后重试');
|
||||
}
|
||||
foreach ($bills as $bill) {
|
||||
if ($bill->status === BillModel::STATUS_PAID) {
|
||||
throw new RepositoryException('账单 ' . $bill->bill_no . ' 已支付,请刷新后重试');
|
||||
}
|
||||
if ((int) $bill->payment_id !== 0) {
|
||||
throw new RepositoryException('账单 ' . $bill->bill_no . ' 已在支付审核中,请勿重复提交');
|
||||
}
|
||||
}
|
||||
|
||||
$amount = $bills->reduce(
|
||||
static fn (string $carry, BillModel $bill): string => bcadd($carry, (string) $bill->total_amount, 2),
|
||||
'0'
|
||||
);
|
||||
|
||||
$payment = PaymentModel::create([
|
||||
'payment_no' => app(BillNumberService::class)->make('ZF'),
|
||||
'store_id' => $store->id,
|
||||
'user_id' => $user->id,
|
||||
'amount' => $amount,
|
||||
'pay_method' => (int) $data['pay_method'],
|
||||
'voucher_ids' => array_map('intval', $data['voucher_ids']),
|
||||
'status' => PaymentModel::STATUS_PENDING,
|
||||
'remark' => (string) ($data['remark'] ?? ''),
|
||||
]);
|
||||
|
||||
// 锁定账单到本支付记录(审核拒绝后释放,可重新付款)
|
||||
BillModel::query()->whereIn('id', $bills->pluck('id'))->update(['payment_id' => $payment->id]);
|
||||
|
||||
return $payment;
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'id' => $payment->id,
|
||||
'payment_no' => $payment->payment_no,
|
||||
'amount' => $payment->amount,
|
||||
], '付款申请已提交,请等待商家审核');
|
||||
}
|
||||
|
||||
/** 支付记录详情(校验归属;含合并账单与凭证图片) */
|
||||
#[GetRoute('/payment/{id}', authorize: true, where: ['id' => '[0-9]+'])]
|
||||
public function detail(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->currentUser($request);
|
||||
$store = $this->ensureStoreBound($user);
|
||||
|
||||
$payment = PaymentModel::query()
|
||||
->where('store_id', $store->id)
|
||||
->find($id);
|
||||
if ($payment === null) {
|
||||
throw new RepositoryException('支付记录不存在');
|
||||
}
|
||||
|
||||
$bills = $payment->bills()
|
||||
->orderBy('id')
|
||||
->get(['id', 'bill_no', 'bill_date', 'product_amount', 'delivery_fee', 'added_amount', 'total_amount', 'status'])
|
||||
->toArray();
|
||||
|
||||
$data = $payment->toArray();
|
||||
$data['voucher_urls'] = $payment->voucherUrls();
|
||||
|
||||
return $this->success([
|
||||
'payment' => $data,
|
||||
'bills' => $bills,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Mini;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\SystemTool\Services\SysFileService;
|
||||
|
||||
/**
|
||||
* 小程序文件上传(汇款凭证等图片)
|
||||
*/
|
||||
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
|
||||
class UploadController extends BaseMiniController
|
||||
{
|
||||
/** 上传图片,返回文件ID与预览地址(5MB 内) */
|
||||
#[PostRoute('/upload', authorize: true)]
|
||||
public function upload(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'file' => 'required|image|max:5120',
|
||||
], [
|
||||
'file.required' => '请选择要上传的图片',
|
||||
'file.image' => '仅支持图片文件',
|
||||
'file.max' => '图片不能超过 5MB',
|
||||
]);
|
||||
|
||||
$user = $this->currentUser($request);
|
||||
// 分组 4=用户上传,渠道 20=APP用户
|
||||
$result = app(SysFileService::class)->upload($data['file'], 4, 20, $user->id);
|
||||
|
||||
return $this->success([
|
||||
'id' => $result['id'],
|
||||
'url' => $result['preview_url'] ?? '',
|
||||
], '上传成功');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Recon;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Models\BillModel;
|
||||
use App\Models\PaymentModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 支付记录(小程序合并付款提交汇款凭证;后台审核:通过后关联账单批量置已支付,拒绝释放账单)
|
||||
*/
|
||||
#[RequestAttribute('/recon/payment', 'recon.payment')]
|
||||
class PaymentController extends BaseController
|
||||
{
|
||||
protected array $searchField = [
|
||||
'store_id' => '=',
|
||||
'status' => '=',
|
||||
'pay_method' => '=',
|
||||
'payment_no' => 'like',
|
||||
];
|
||||
|
||||
/** 支付记录列表(待审核优先) */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$pageSize = $params['pageSize'] ?? 10;
|
||||
$data = $this->buildSearch($params, PaymentModel::query()
|
||||
->with(['store:id,name', 'user:id,nickname', 'auditor:id,nickname'])
|
||||
->withCount('bills'))
|
||||
->orderBy('status')
|
||||
->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
|
||||
{
|
||||
$payment = PaymentModel::with(['store:id,name,contact,phone', 'user:id,nickname', 'auditor:id,nickname'])->find($id);
|
||||
if (empty($payment)) {
|
||||
throw new RepositoryException('支付记录不存在');
|
||||
}
|
||||
|
||||
$bills = $payment->bills()
|
||||
->orderBy('id')
|
||||
->get(['id', 'bill_no', 'bill_date', 'product_amount', 'delivery_fee', 'added_amount', 'total_amount', 'status'])
|
||||
->toArray();
|
||||
|
||||
$data = $payment->toArray();
|
||||
$data['voucher_urls'] = $payment->voucherUrls();
|
||||
|
||||
return $this->success([
|
||||
'payment' => $data,
|
||||
'bills' => $bills,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核支付记录:通过 → 关联账单全部置已支付;拒绝 → 释放账单(可重新发起付款)
|
||||
* @throws Throwable
|
||||
*/
|
||||
#[PutRoute(route: '/{id}/audit', authorize: 'audit', where: ['id' => '[0-9]+'])]
|
||||
public function audit(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'result' => 'required|string|in:pass,reject',
|
||||
'audit_remark' => 'nullable|string|max:255|required_if:result,reject',
|
||||
], [
|
||||
'result.required' => '请选择审核结果',
|
||||
'result.in' => '审核结果不正确',
|
||||
'audit_remark.required_if' => '拒绝时请填写原因',
|
||||
'audit_remark.max' => '审核备注超过最大长度',
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $request) {
|
||||
$payment = PaymentModel::query()->lockForUpdate()->find($id);
|
||||
if (empty($payment)) {
|
||||
throw new RepositoryException('支付记录不存在');
|
||||
}
|
||||
if ($payment->status !== PaymentModel::STATUS_PENDING) {
|
||||
throw new RepositoryException('该支付记录已审核,请勿重复操作');
|
||||
}
|
||||
|
||||
$bills = $payment->bills()->lockForUpdate()->get();
|
||||
$auditorId = (int) $request->user()->id;
|
||||
$now = now();
|
||||
|
||||
if ($data['result'] === 'pass') {
|
||||
// 任一账单已通过其他方式收款(如线下登记)则整批中止,避免重复收款
|
||||
$paid = $bills->where('status', BillModel::STATUS_PAID);
|
||||
if ($paid->isNotEmpty()) {
|
||||
throw new RepositoryException(
|
||||
'账单 ' . $paid->pluck('bill_no')->implode('、') . ' 已收款,请核实后再审核'
|
||||
);
|
||||
}
|
||||
|
||||
$methodName = PaymentModel::METHOD_NAMES[$payment->pay_method] ?? '线上支付';
|
||||
BillModel::query()->whereIn('id', $bills->pluck('id'))->update([
|
||||
'status' => BillModel::STATUS_PAID,
|
||||
'paid_at' => $now,
|
||||
'paid_operator_id' => $auditorId,
|
||||
'pay_remark' => $methodName . '(支付单号 ' . $payment->payment_no . ')',
|
||||
]);
|
||||
$payment->status = PaymentModel::STATUS_APPROVED;
|
||||
} else {
|
||||
// 拒绝:释放账单,门店可重新发起付款
|
||||
BillModel::query()->whereIn('id', $bills->pluck('id'))->update(['payment_id' => 0]);
|
||||
$payment->status = PaymentModel::STATUS_REJECTED;
|
||||
}
|
||||
|
||||
$payment->audited_at = $now;
|
||||
$payment->auditor_id = $auditorId;
|
||||
$payment->audit_remark = (string) ($data['audit_remark'] ?? '');
|
||||
$payment->save();
|
||||
|
||||
return $this->success(
|
||||
[],
|
||||
$payment->status === PaymentModel::STATUS_APPROVED
|
||||
? '审核通过,' . $bills->count() . ' 张账单已置为已支付'
|
||||
: '已拒绝,账单已释放可重新付款'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class BillModel extends Model
|
||||
'added_amount',
|
||||
'total_amount',
|
||||
'status',
|
||||
'payment_id',
|
||||
'paid_at',
|
||||
'pay_remark',
|
||||
'paid_operator_id',
|
||||
@@ -63,6 +64,7 @@ class BillModel extends Model
|
||||
'added_amount' => 'decimal:2',
|
||||
'total_amount' => 'decimal:2',
|
||||
'status' => 'integer',
|
||||
'payment_id' => 'integer',
|
||||
'paid_at' => 'datetime:Y-m-d H:i:s',
|
||||
'paid_operator_id' => 'integer',
|
||||
'operator_id' => 'integer',
|
||||
@@ -101,6 +103,14 @@ class BillModel extends Model
|
||||
return $this->belongsTo(SysUserModel::class, 'paid_operator_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联支付记录(小程序合并付款)
|
||||
*/
|
||||
public function payment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PaymentModel::class, 'payment_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 本账单关联的门店订单
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
|
||||
/**
|
||||
* 支付记录模型(小程序选择门店账单合并付款,提交汇款凭证;后台审核通过后关联账单批量置已支付)
|
||||
*/
|
||||
class PaymentModel extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** 支付方式:微信 */
|
||||
public const int METHOD_WECHAT = 1;
|
||||
/** 支付方式:支付宝 */
|
||||
public const int METHOD_ALIPAY = 2;
|
||||
/** 支付方式:对公汇款(银行卡) */
|
||||
public const int METHOD_BANK = 3;
|
||||
|
||||
/** 支付方式中文名 */
|
||||
public const array METHOD_NAMES = [
|
||||
self::METHOD_WECHAT => '微信支付',
|
||||
self::METHOD_ALIPAY => '支付宝',
|
||||
self::METHOD_BANK => '对公汇款',
|
||||
];
|
||||
|
||||
/** 状态:待审核 */
|
||||
public const int STATUS_PENDING = 0;
|
||||
/** 状态:已通过 */
|
||||
public const int STATUS_APPROVED = 1;
|
||||
/** 状态:已拒绝 */
|
||||
public const int STATUS_REJECTED = 2;
|
||||
|
||||
/** 状态中文名 */
|
||||
public const array STATUS_NAMES = [
|
||||
self::STATUS_PENDING => '待审核',
|
||||
self::STATUS_APPROVED => '已通过',
|
||||
self::STATUS_REJECTED => '已拒绝',
|
||||
];
|
||||
|
||||
protected $table = 'payment';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'payment_no',
|
||||
'store_id',
|
||||
'user_id',
|
||||
'amount',
|
||||
'pay_method',
|
||||
'voucher_ids',
|
||||
'status',
|
||||
'remark',
|
||||
'audited_at',
|
||||
'auditor_id',
|
||||
'audit_remark',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'store_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'amount' => 'decimal:2',
|
||||
'pay_method' => 'integer',
|
||||
'status' => 'integer',
|
||||
'audited_at' => 'datetime:Y-m-d H:i:s',
|
||||
'auditor_id' => 'integer',
|
||||
'created_at' => 'datetime:Y-m-d H:i:s',
|
||||
];
|
||||
|
||||
/**
|
||||
* 汇款凭证图片ID(逗号分隔字符串 ↔ 数组)
|
||||
*/
|
||||
public function voucherIds(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn ($value) => $value === '' || $value === null ? [] : explode(',', (string) $value),
|
||||
set: fn ($value) => is_array($value) ? implode(',', $value) : $value,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇款凭证图片URL列表(保持提交顺序)
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function voucherUrls(): array
|
||||
{
|
||||
$ids = array_map('intval', $this->voucher_ids);
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
$urls = SysFileModel::query()->whereIn('id', $ids)->pluck('preview_url', 'id');
|
||||
$result = [];
|
||||
foreach ($ids as $id) {
|
||||
if (isset($urls[$id])) {
|
||||
$result[] = $urls[$id];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 所属门店(含软删除门店,保证历史记录可见)
|
||||
*/
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StoreModel::class, 'store_id', 'id')->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 本支付记录合并付款的账单
|
||||
*/
|
||||
public function bills(): HasMany
|
||||
{
|
||||
return $this->hasMany(BillModel::class, 'payment_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交人(小程序用户)
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserModel::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核人(后台系统用户)
|
||||
*/
|
||||
public function auditor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysUserModel::class, 'auditor_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -27,12 +27,13 @@ class BillNumberService
|
||||
'RC' => ['reconciliation', 'recon_no'],
|
||||
'JS' => ['settlement', 'settlement_no'],
|
||||
'ZD' => ['bill', 'bill_no'],
|
||||
'ZF' => ['payment', 'payment_no'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 生成业务单号
|
||||
*
|
||||
* @param string $prefix 业务前缀:PO 采购单 / SO 订货单 / RC 对账 / JS 结算 / ZD 账单
|
||||
* @param string $prefix 业务前缀:PO 采购单 / SO 订货单 / RC 对账 / JS 结算 / ZD 账单 / ZF 支付
|
||||
* @return string 如 PO202607230001
|
||||
*/
|
||||
public function make(string $prefix): string
|
||||
|
||||
@@ -30,6 +30,7 @@ return new class extends Migration
|
||||
$table->decimal('added_amount', 10, 2)->default(0)->comment('附加金额(周转筐/托盘金额)');
|
||||
$table->decimal('total_amount', 10, 2)->default(0)->comment('账单总金额 = 商品金额 + 配送费 + 附加金额');
|
||||
$table->integer('status')->default(0)->comment('支付状态(0未支付 1已支付)');
|
||||
$table->integer('payment_id')->default(0)->comment('关联支付记录ID(0=未发起支付)');
|
||||
$table->timestamp('paid_at')->nullable()->comment('付款时间(线下收款手动登记)');
|
||||
$table->string('pay_remark', 255)->default('')->comment('付款备注(线下收款信息)');
|
||||
$table->integer('paid_operator_id')->default(0)->comment('收款操作人(后台系统用户ID)');
|
||||
@@ -39,6 +40,7 @@ return new class extends Migration
|
||||
$table->unique(['purchase_id', 'store_id'], 'bill_purchase_store_unique');
|
||||
$table->index(['store_id', 'bill_date'], 'bill_store_date_index');
|
||||
$table->index(['status'], 'bill_status_index');
|
||||
$table->index(['payment_id'], 'bill_payment_index');
|
||||
$table->comment('门店账单表(采购单完成后按门店生成)');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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
|
||||
{
|
||||
if (! Schema::hasTable('payment')) {
|
||||
Schema::create('payment', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('支付记录ID');
|
||||
$table->string('payment_no', 32)->unique()->comment('支付单号');
|
||||
$table->integer('store_id')->comment('门店ID');
|
||||
$table->integer('user_id')->default(0)->comment('提交人(小程序用户ID)');
|
||||
$table->decimal('amount', 10, 2)->default(0)->comment('支付金额(= 关联账单总金额合计,提交时快照)');
|
||||
$table->integer('pay_method')->comment('支付方式(1微信 2支付宝 3对公汇款)');
|
||||
$table->string('voucher_ids', 255)->default('')->comment('汇款凭证图片ID(逗号分隔)');
|
||||
$table->integer('status')->default(0)->comment('状态(0待审核 1已通过 2已拒绝)');
|
||||
$table->string('remark', 255)->default('')->comment('门店备注');
|
||||
$table->timestamp('audited_at')->nullable()->comment('审核时间');
|
||||
$table->integer('auditor_id')->default(0)->comment('审核人(后台系统用户ID)');
|
||||
$table->string('audit_remark', 255)->default('')->comment('审核备注(拒绝原因)');
|
||||
$table->timestamps();
|
||||
$table->index(['store_id', 'status'], 'payment_store_status_index');
|
||||
$table->comment('支付记录表(小程序合并付款,后台审核汇款凭证)');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('payment');
|
||||
}
|
||||
};
|
||||
@@ -242,6 +242,16 @@ class PermissionSeeder extends Seeder
|
||||
['type' => 'rule', 'key' => 'recon.containerReturn.delete', 'name' => '删除'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'route',
|
||||
'key' => 'recon.payment',
|
||||
'name' => '支付记录',
|
||||
'path' => '/recon/payment',
|
||||
'children' => [
|
||||
['type' => 'rule', 'key' => 'recon.payment.query', 'name' => '查询'],
|
||||
['type' => 'rule', 'key' => 'recon.payment.audit', 'name' => '审核'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'route',
|
||||
'key' => 'recon.settlement',
|
||||
|
||||
@@ -17,7 +17,8 @@ class SysDataSeeder extends Seeder
|
||||
DB::table('sys_site_config_group')->insert([
|
||||
['id' => 1, 'title' => '网站设置', 'key' => 'web', 'remark' => '网站基础设置', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 2, 'title' => '小程序设置', 'key' => 'wechatMini', 'remark' => '小程序设置', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 3, 'title' => '支付配置', 'key' => 'pay', 'remark' => '网站的支付配置', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 3, 'title' => '业务配置', 'key' => 'services', 'remark' => '小程序设置', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 4, 'title' => '支付配置', 'key' => 'pay', 'remark' => '网站的支付配置', 'created_at' => $date, 'updated_at' => $date],
|
||||
]);
|
||||
DB::table('sys_site_config_items')->insert([
|
||||
['id' => 1, 'group_id' => 1, 'key' => 'title', 'title' => '网站标题', 'describe' => '网站标题,用于展示在网站logo旁边和登录页面以及网页title中', 'values' => 'Xin Admin', 'type' => 'Input','options' => "", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date,],
|
||||
@@ -26,6 +27,11 @@ class SysDataSeeder extends Seeder
|
||||
['id' => 4, 'group_id' => 1, 'key' => 'describe', 'title' => '网站描述', 'describe' => '网站的基本描述', 'values' => '没有描述', 'type' => 'TextArea','options' => "", 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 5, 'group_id' => 2, 'key' => 'appid', 'title' => 'APPID', 'describe' => '小程序的APPID', 'values' => '没有描述', 'type' => 'Input','options' => "", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 6, 'group_id' => 2, 'key' => 'secret', 'title' => 'SecretKey', 'describe' => '小程序的SecretKey', 'values' => '没有描述', 'type' => 'Input','options' => "", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 7, 'group_id' => 3, 'key' => 'box_amount', 'title' => '周转筐金额', 'describe' => '周转筐的金额,用于附加业务金额的计算', 'values' => '', 'type' => 'InputNumber','props' => "min=0\nmax=10000\nstep=0.01", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 8, 'group_id' => 3, 'key' => 'tray_amount', 'title' => '周转托盘金额', 'describe' => '周转托盘的金额,用于附加金额的计算', 'values' => '', 'type' => 'InputNumber','props' => "min=0\nmax=10000\nstep=0.01", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 9, 'group_id' => 4, 'key' => 'wechat_qrcode', 'title' => '微信收款码', 'describe' => '微信收款码图片地址(在文件管理中上传收款码后复制图片链接,或直接填写文件ID),小程序付款页展示', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 10, 'group_id' => 4, 'key' => 'alipay_qrcode', 'title' => '支付宝收款码', 'describe' => '支付宝收款码图片地址(在文件管理中上传收款码后复制图片链接,或直接填写文件ID),小程序付款页展示', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 11, 'group_id' => 4, 'key' => 'bank_info', 'title' => '对公汇款信息', 'describe' => '对公账户汇款信息(户名、账号、开户行等),小程序付款页展示', 'values' => '', 'type' => 'TextArea','options' => "", 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
]);
|
||||
// 字典类型初始数据
|
||||
DB::table('sys_dict')->insert([
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import createAxios from '@/utils/request';
|
||||
import type { IPaymentDetail } from '@/domain/iPayment.ts';
|
||||
|
||||
/** 支付记录详情(支付信息 + 凭证图片 + 合并账单) */
|
||||
export async function getPaymentDetail(id: number) {
|
||||
return createAxios<IPaymentDetail>({
|
||||
url: `/recon/payment/${id}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/** 审核支付记录:pass 通过(账单批量置已支付)/ reject 拒绝(释放账单,需填原因) */
|
||||
export async function auditPayment(id: number, data: { result: 'pass' | 'reject'; audit_remark?: string }) {
|
||||
return createAxios({
|
||||
url: `/recon/payment/${id}/audit`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/** 支付记录(小程序合并付款提交汇款凭证,后台审核) */
|
||||
export default interface IPayment {
|
||||
id?: number;
|
||||
payment_no?: string;
|
||||
store_id?: number;
|
||||
user_id?: number;
|
||||
/** 支付金额(= 关联账单总金额合计) */
|
||||
amount?: string;
|
||||
/** 支付方式:1微信 2支付宝 3对公汇款 */
|
||||
pay_method?: number;
|
||||
/** 汇款凭证图片ID列表 */
|
||||
voucher_ids?: number[];
|
||||
/** 凭证图片URL列表(详情接口解析) */
|
||||
voucher_urls?: string[];
|
||||
/** 状态:0待审核 1已通过 2已拒绝 */
|
||||
status?: number;
|
||||
/** 门店备注 */
|
||||
remark?: string;
|
||||
audited_at?: string | null;
|
||||
auditor_id?: number;
|
||||
audit_remark?: string;
|
||||
created_at?: string;
|
||||
/** 列表/详情接口附带 */
|
||||
store?: { id: number; name: string; contact?: string; phone?: string } | null;
|
||||
user?: { id: number; nickname: string } | null;
|
||||
auditor?: { id: number; nickname: string } | null;
|
||||
bills_count?: number;
|
||||
}
|
||||
|
||||
/** 支付记录关联账单(合并付款) */
|
||||
export interface IPaymentBill {
|
||||
id: number;
|
||||
bill_no: string;
|
||||
bill_date: string;
|
||||
product_amount: string;
|
||||
delivery_fee: string;
|
||||
added_amount: string;
|
||||
total_amount: string;
|
||||
/** 0未支付 1已支付 */
|
||||
status: number;
|
||||
}
|
||||
|
||||
/** 支付记录详情 */
|
||||
export interface IPaymentDetail {
|
||||
payment: IPayment;
|
||||
bills: IPaymentBill[];
|
||||
}
|
||||
|
||||
/** 支付方式映射 */
|
||||
export const PAY_METHOD_MAP: Record<number, { text: string; color: string }> = {
|
||||
1: { text: '微信支付', color: 'green' },
|
||||
2: { text: '支付宝', color: 'blue' },
|
||||
3: { text: '对公汇款', color: 'purple' },
|
||||
};
|
||||
|
||||
/** 支付记录状态映射 */
|
||||
export const PAYMENT_STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||
0: { text: '待审核', color: 'warning' },
|
||||
1: { text: '已通过', color: 'success' },
|
||||
2: { text: '已拒绝', color: 'error' },
|
||||
};
|
||||
@@ -76,6 +76,8 @@ export interface IBill {
|
||||
total_amount: string;
|
||||
/** 支付状态:0未支付 1已支付 */
|
||||
status?: number;
|
||||
/** 关联支付记录ID(0=未发起支付) */
|
||||
payment_id?: number;
|
||||
/** 付款时间(线下收款手动登记) */
|
||||
paid_at?: string | null;
|
||||
/** 付款备注(线下收款信息) */
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Radio,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import { UnorderedListOutlined } from '@ant-design/icons';
|
||||
import XinTable from '@/components/XinTable';
|
||||
import type {
|
||||
XinTableColumn,
|
||||
XinTableInstance,
|
||||
XinTableProps,
|
||||
} from '@/components/XinTable/typings.ts';
|
||||
import type IPayment from '@/domain/iPayment.ts';
|
||||
import type { IPaymentBill, IPaymentDetail } from '@/domain/iPayment.ts';
|
||||
import { PAY_METHOD_MAP, PAYMENT_STATUS_MAP } from '@/domain/iPayment.ts';
|
||||
import { BILL_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
|
||||
import { getPaymentDetail, auditPayment } from '@/api/recon/payment.ts';
|
||||
import { getStoreOptions } from '@/api/customer/store.ts';
|
||||
import type IStore from '@/domain/iStore.ts';
|
||||
import AuthButton from '@/components/AuthButton';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/** 审核表单 */
|
||||
interface AuditFormValues {
|
||||
result: 'pass' | 'reject';
|
||||
audit_remark?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付记录(小程序合并付款提交汇款凭证;审核通过后关联账单全部置已支付,拒绝则释放账单)
|
||||
*/
|
||||
const PaymentPage: React.FC = () => {
|
||||
const tableRef = useRef<XinTableInstance<IPayment>>(null);
|
||||
const [stores, setStores] = useState<IStore[]>([]);
|
||||
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<IPaymentDetail | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
// 审核弹窗
|
||||
const [auditTarget, setAuditTarget] = useState<IPayment | null>(null);
|
||||
const [auditSaving, setAuditSaving] = useState(false);
|
||||
const [auditForm] = Form.useForm<AuditFormValues>();
|
||||
const watchAuditResult = Form.useWatch('result', auditForm);
|
||||
|
||||
useEffect(() => {
|
||||
getStoreOptions().then((res) => setStores(res.data.data ?? []));
|
||||
}, []);
|
||||
|
||||
const openDetail = async (id: number) => {
|
||||
setDetailOpen(true);
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const res = await getPaymentDetail(id);
|
||||
setDetail(res.data.data ?? null);
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 打开审核弹窗 */
|
||||
const openAudit = (record: IPayment) => {
|
||||
setAuditTarget(record);
|
||||
auditForm.setFieldsValue({ result: 'pass', audit_remark: '' });
|
||||
};
|
||||
|
||||
/** 提交审核:通过 → 账单批量置已支付;拒绝 → 释放账单 */
|
||||
const handleAuditSave = async (values: AuditFormValues) => {
|
||||
if (!auditTarget?.id) {
|
||||
return;
|
||||
}
|
||||
setAuditSaving(true);
|
||||
try {
|
||||
const res = await auditPayment(auditTarget.id, values);
|
||||
message.success(res.data.msg ?? '审核完成');
|
||||
setAuditTarget(null);
|
||||
await tableRef.current?.reload();
|
||||
if (detail && detail.payment.id === auditTarget.id) {
|
||||
await openDetail(auditTarget.id);
|
||||
}
|
||||
} finally {
|
||||
setAuditSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 合并账单列 */
|
||||
const billColumns: TableProps<IPaymentBill>['columns'] = [
|
||||
{
|
||||
title: '账单号',
|
||||
dataIndex: 'bill_no',
|
||||
align: 'center',
|
||||
render: (v) => <Text copyable={{ text: v }}>{v}</Text>,
|
||||
},
|
||||
{ title: '账单日期', dataIndex: 'bill_date', align: 'center' },
|
||||
{
|
||||
title: '商品金额',
|
||||
dataIndex: 'product_amount',
|
||||
align: 'center',
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '配送费',
|
||||
dataIndex: 'delivery_fee',
|
||||
align: 'center',
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '附加金额',
|
||||
dataIndex: 'added_amount',
|
||||
align: 'center',
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '账单总金额',
|
||||
dataIndex: 'total_amount',
|
||||
align: 'center',
|
||||
render: (v) => <Text strong>¥{Number(v).toFixed(2)}</Text>,
|
||||
},
|
||||
{
|
||||
title: '支付状态',
|
||||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
render: (v) => {
|
||||
const item = BILL_STATUS_MAP[Number(v ?? 0)];
|
||||
return <Tag color={item?.color}>{item?.text}</Tag>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const columns: XinTableColumn<IPayment>[] = [
|
||||
{
|
||||
title: '支付单号',
|
||||
dataIndex: 'payment_no',
|
||||
valueType: 'text',
|
||||
hideInForm: true,
|
||||
width: 210,
|
||||
render: (_, record) => <Text copyable={{ text: record.payment_no }}>{record.payment_no}</Text>,
|
||||
},
|
||||
{
|
||||
title: '门店',
|
||||
dataIndex: 'store_id',
|
||||
valueType: 'select',
|
||||
hideInForm: true,
|
||||
fieldProps: {
|
||||
options: stores.map((s) => ({ label: s.name, value: s.id })),
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
},
|
||||
render: (_, record) => record.store?.name ?? `门店#${record.store_id}`,
|
||||
},
|
||||
{
|
||||
title: '支付金额',
|
||||
dataIndex: 'amount',
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
align: 'center',
|
||||
render: (_, record) => <Text strong type="danger">¥{record.amount}</Text>,
|
||||
},
|
||||
{
|
||||
title: '支付方式',
|
||||
dataIndex: 'pay_method',
|
||||
valueType: 'select',
|
||||
hideInForm: true,
|
||||
align: 'center',
|
||||
fieldProps: {
|
||||
options: Object.entries(PAY_METHOD_MAP).map(([value, item]) => ({
|
||||
value: Number(value),
|
||||
label: item.text,
|
||||
})),
|
||||
},
|
||||
render: (_, record) => {
|
||||
const item = PAY_METHOD_MAP[record.pay_method ?? 0];
|
||||
return <Tag color={item?.color}>{item?.text ?? '-'}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '合并账单',
|
||||
dataIndex: 'bills_count',
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
align: 'center',
|
||||
render: (_, record) => `${record.bills_count ?? 0} 张`,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
valueType: 'select',
|
||||
hideInForm: true,
|
||||
align: 'center',
|
||||
fieldProps: {
|
||||
options: Object.entries(PAYMENT_STATUS_MAP).map(([value, item]) => ({
|
||||
value: Number(value),
|
||||
label: item.text,
|
||||
})),
|
||||
},
|
||||
render: (_, record) => {
|
||||
const item = PAYMENT_STATUS_MAP[record.status ?? 0];
|
||||
return <Tag color={item?.color}>{item?.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '提交人',
|
||||
dataIndex: 'user',
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
align: 'center',
|
||||
render: (_, record) => record.user?.nickname ?? '-',
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'created_at',
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '审核人',
|
||||
dataIndex: 'auditor',
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
align: 'center',
|
||||
render: (_, record) => record.auditor?.nickname ?? '-',
|
||||
},
|
||||
];
|
||||
|
||||
const operateRender: XinTableProps<IPayment>['operateRender'] = (record) => [
|
||||
<Button
|
||||
key="detail"
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<UnorderedListOutlined />}
|
||||
onClick={() => openDetail(record.id!)}
|
||||
/>,
|
||||
record.status === 0 ? (
|
||||
<AuthButton key="audit" auth="recon.payment.audit">
|
||||
<Button size="small" variant="solid" color="orange" onClick={() => openAudit(record)}>
|
||||
审核
|
||||
</Button>
|
||||
</AuthButton>
|
||||
) : null,
|
||||
];
|
||||
|
||||
const tableProps: XinTableProps<IPayment> = {
|
||||
api: '/recon/payment',
|
||||
columns,
|
||||
rowKey: 'id',
|
||||
accessName: 'recon.payment',
|
||||
tableRef,
|
||||
operateRender,
|
||||
formProps: false,
|
||||
actionBarRender: (dom) => [dom.search, dom.keywordSearch],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5">
|
||||
<Title level={3}>支付记录</Title>
|
||||
<Text type="secondary">
|
||||
门店在小程序端选择账单合并付款并上传汇款凭证;审核通过后关联账单全部置为已支付,拒绝则释放账单可重新付款。
|
||||
</Text>
|
||||
</div>
|
||||
<XinTable<IPayment> {...tableProps} />
|
||||
|
||||
{/* 支付详情:支付信息 + 凭证 + 合并账单 */}
|
||||
<Drawer
|
||||
title={detail ? `支付单 ${detail.payment.payment_no}` : '支付记录详情'}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
size={1000}
|
||||
loading={detailLoading}
|
||||
footer={
|
||||
detail && detail.payment.status === 0 ? (
|
||||
<Space className="flex justify-end">
|
||||
<AuthButton auth="recon.payment.audit">
|
||||
<Button type="primary" onClick={() => openAudit(detail.payment)}>
|
||||
审核
|
||||
</Button>
|
||||
</AuthButton>
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{detail ? (
|
||||
<>
|
||||
<Descriptions column={3} size="small" bordered>
|
||||
<Descriptions.Item label="门店">{detail.payment.store?.name ?? `门店#${detail.payment.store_id}`}</Descriptions.Item>
|
||||
<Descriptions.Item label="支付金额">
|
||||
<Text strong type="danger">¥{detail.payment.amount}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="支付方式">
|
||||
<Tag color={PAY_METHOD_MAP[detail.payment.pay_method ?? 0]?.color}>
|
||||
{PAY_METHOD_MAP[detail.payment.pay_method ?? 0]?.text ?? '-'}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={PAYMENT_STATUS_MAP[detail.payment.status ?? 0]?.color}>
|
||||
{PAYMENT_STATUS_MAP[detail.payment.status ?? 0]?.text}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提交人">{detail.payment.user?.nickname ?? '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="提交时间">{detail.payment.created_at}</Descriptions.Item>
|
||||
<Descriptions.Item label="审核人">{detail.payment.auditor?.nickname ?? '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="审核时间">{detail.payment.audited_at ?? '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="审核备注">{detail.payment.audit_remark || '-'}</Descriptions.Item>
|
||||
{detail.payment.remark ? (
|
||||
<Descriptions.Item label="门店备注" span={3}>{detail.payment.remark}</Descriptions.Item>
|
||||
) : null}
|
||||
</Descriptions>
|
||||
|
||||
<Title level={5} className="mt-6! mb-3!">
|
||||
汇款凭证
|
||||
</Title>
|
||||
{(detail.payment.voucher_urls ?? []).length > 0 ? (
|
||||
<Image.PreviewGroup>
|
||||
<Space wrap size={12}>
|
||||
{(detail.payment.voucher_urls ?? []).map((url, index) => (
|
||||
<Image
|
||||
key={index}
|
||||
src={url}
|
||||
width={120}
|
||||
height={120}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
) : (
|
||||
<Text type="secondary">无凭证</Text>
|
||||
)}
|
||||
|
||||
<Title level={5} className="mt-6! mb-3!">
|
||||
合并付款账单({detail.bills.length} 张)
|
||||
</Title>
|
||||
<Table<IPaymentBill>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
bordered
|
||||
columns={billColumns}
|
||||
dataSource={detail.bills}
|
||||
pagination={false}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Drawer>
|
||||
|
||||
{/* 审核弹窗 */}
|
||||
<Modal
|
||||
title={auditTarget ? `审核支付单 ${auditTarget.payment_no}` : '审核'}
|
||||
open={auditTarget !== null}
|
||||
onCancel={() => setAuditTarget(null)}
|
||||
onOk={() => auditForm.submit()}
|
||||
confirmLoading={auditSaving}
|
||||
okText="提交审核"
|
||||
destroyOnHidden
|
||||
>
|
||||
<div className="py-2 text-gray-500">
|
||||
支付金额 <Text strong type="danger">¥{auditTarget?.amount ?? '0.00'}</Text>({auditTarget?.bills_count ?? 0} 张账单);
|
||||
通过后关联账单全部置为「已支付」,拒绝则释放账单,门店可重新发起付款。
|
||||
</div>
|
||||
<Form form={auditForm} layout="vertical" onFinish={handleAuditSave}>
|
||||
<Form.Item label="审核结果" name="result" rules={[{ required: true, message: '请选择审核结果' }]}>
|
||||
<Radio.Group
|
||||
options={[
|
||||
{ value: 'pass', label: '通过(账单置为已支付)' },
|
||||
{ value: 'reject', label: '拒绝(释放账单)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={watchAuditResult === 'reject' ? '拒绝原因' : '审核备注'}
|
||||
name="audit_remark"
|
||||
rules={[
|
||||
{ required: watchAuditResult === 'reject', message: '拒绝时请填写原因' },
|
||||
{ max: 255 },
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={2} maxLength={255} placeholder="审核备注(拒绝时必填)" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentPage;
|
||||
Reference in New Issue
Block a user