feat: 新增发票抬头、晒单、钱包日志等模型及后台管理功能
- 新增 invoice_title、showcase、wallet_log 模型及对应语言包 - 新增后台控制器、视图及搜索字段配置 - 修复服务类目删除保护逻辑 - 优化后台登录页面文案及样式 - 新增前端角色切换逻辑及页面拦截 - 新增 SVG 图标及样式变量 - 修复接口权限及下拉框默认值问题
这个提交包含在:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\library\InvoiceService;
|
||||
use app\common\library\OrderStatus;
|
||||
use think\Db;
|
||||
|
||||
@@ -85,35 +86,44 @@ class Merchant extends Base
|
||||
|
||||
$uids = $this->merchantUserIds($merchant);
|
||||
$isOwner = (int)$merchant['user_id'] === $this->uid();
|
||||
$orderCount = (int)Db::name('order')->where('user_id', 'in', $uids)->where('pay_status', 1)->count();
|
||||
$totalAmount = (float)Db::name('order')->where('user_id', 'in', $uids)->where('pay_status', 1)->sum('pay_amount');
|
||||
$invoiced = (float)Db::name('invoice')->where('merchant_id', $merchant['id'])->where('status', 'issued')->sum('amount');
|
||||
$orderCount = (int)Db::name('order')->where('user_id', 'in', $uids)->where('pay_status', 1)->where('status', '<>', OrderStatus::CANCELLED)->count();
|
||||
$quota = InvoiceService::getQuotaSummary($this->uid());
|
||||
$totalAmount = (float)$quota['paid_amount'];
|
||||
$invoiced = (float)$quota['used_amount'];
|
||||
|
||||
$this->success('', [
|
||||
'applied' => true,
|
||||
'is_owner' => $isOwner,
|
||||
'role' => $isOwner ? 'owner' : 'member',
|
||||
'merchant' => [
|
||||
'id' => (int)$merchant['id'],
|
||||
'name' => $merchant['name'],
|
||||
'license_no' => $merchant['license_no'],
|
||||
'legal_person' => $merchant['legal_person'],
|
||||
'contact' => $merchant['contact'],
|
||||
'mobile' => $merchant['mobile'],
|
||||
'address' => $merchant['province'] . $merchant['city'] . $merchant['district'] . $merchant['address'],
|
||||
'invoice_title' => $merchant['invoice_title'],
|
||||
'invoice_tax_no' => $merchant['invoice_tax_no'],
|
||||
'settle_type' => $merchant['settle_type'],
|
||||
'verify_status' => $merchant['verify_status'],
|
||||
'audit_msg' => $merchant['audit_msg'],
|
||||
'role' => $isOwner ? 'owner' : 'member',
|
||||
'is_owner' => $isOwner,
|
||||
'owner_user_id' => (int)$merchant['user_id'],
|
||||
'member_count' => max(0, count($uids) - 1),
|
||||
'order_count' => $orderCount,
|
||||
'total_amount' => round($totalAmount, 2),
|
||||
'invoiced_amount' => round($invoiced, 2),
|
||||
'invoiceable' => round(max(0, $totalAmount - $invoiced), 2),
|
||||
'id' => (int)$merchant['id'],
|
||||
'name' => $merchant['name'],
|
||||
'license_no' => $merchant['license_no'],
|
||||
'legal_person' => $merchant['legal_person'],
|
||||
'contact' => $merchant['contact'],
|
||||
'mobile' => $merchant['mobile'],
|
||||
'province' => (string)$merchant['province'],
|
||||
'city' => (string)$merchant['city'],
|
||||
'district' => (string)$merchant['district'],
|
||||
'detail_address' => (string)$merchant['address'],
|
||||
'address' => $merchant['province'] . $merchant['city'] . $merchant['district'] . $merchant['address'],
|
||||
'invoice_title' => $merchant['invoice_title'],
|
||||
'invoice_tax_no' => $merchant['invoice_tax_no'],
|
||||
'invoice_bank' => $merchant['invoice_bank'],
|
||||
'invoice_account' => $merchant['invoice_account'],
|
||||
'settle_type' => $merchant['settle_type'],
|
||||
'verify_status' => $merchant['verify_status'],
|
||||
'audit_msg' => $merchant['audit_msg'],
|
||||
'role' => $isOwner ? 'owner' : 'member',
|
||||
'is_owner' => $isOwner,
|
||||
'owner_user_id' => (int)$merchant['user_id'],
|
||||
'member_count' => max(0, count($uids) - 1),
|
||||
'order_count' => $orderCount,
|
||||
'total_amount' => round($totalAmount, 2),
|
||||
'invoiced_amount' => round($invoiced, 2),
|
||||
'issued_amount' => round((float)$quota['issued_amount'], 2),
|
||||
'wait_invoice_amount' => round((float)$quota['wait_amount'], 2),
|
||||
'invoiceable' => round((float)$quota['available'], 2),
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -139,6 +149,23 @@ class Merchant extends Base
|
||||
$this->error('联系电话格式不正确');
|
||||
}
|
||||
Db::name('merchant')->where('id', $merchant['id'])->update($data);
|
||||
|
||||
// 若维护了商家开票资料,同步刷新关联的商家默认发票抬头
|
||||
if (isset($data['invoice_title']) || isset($data['invoice_tax_no']) || isset($data['invoice_bank']) || isset($data['invoice_account'])) {
|
||||
$latest = Db::name('merchant')->where('id', $merchant['id'])->find();
|
||||
if ($latest) {
|
||||
$syncFields = ['updatetime' => time()];
|
||||
if (!empty($latest['invoice_title'])) $syncFields['title'] = $latest['invoice_title'];
|
||||
if (!empty($latest['invoice_tax_no'])) $syncFields['tax_no'] = strtoupper($latest['invoice_tax_no']);
|
||||
if (isset($data['invoice_bank'])) $syncFields['bank_name'] = $latest['invoice_bank'];
|
||||
if (isset($data['invoice_account'])) $syncFields['bank_account'] = $latest['invoice_account'];
|
||||
Db::name('invoice_title')
|
||||
->where('merchant_id', (int)$merchant['id'])
|
||||
->where('title_type', 'enterprise')
|
||||
->update($syncFields);
|
||||
}
|
||||
}
|
||||
|
||||
$this->success('已保存');
|
||||
}
|
||||
|
||||
@@ -181,11 +208,22 @@ class Merchant extends Base
|
||||
$uids = $this->merchantUserIds($merchant);
|
||||
$status = $this->request->param('status', 'all');
|
||||
$keyword = trim((string)$this->request->param('keyword', ''));
|
||||
$memberUid = (int)$this->request->param('member_uid', 0);
|
||||
$month = trim((string)$this->request->param('month', ''));
|
||||
$page = max(1, (int)$this->request->param('page', 1));
|
||||
$limit = min(50, max(1, (int)$this->request->param('limit', 20)));
|
||||
|
||||
$build = function () use ($uids, $status, $keyword) {
|
||||
$q = Db::name('order')->where('user_id', 'in', $uids);
|
||||
$targetUids = ($memberUid > 0 && in_array($memberUid, $uids, true)) ? [$memberUid] : $uids;
|
||||
|
||||
$build = function () use ($targetUids, $status, $keyword, $month) {
|
||||
$q = Db::name('order')->where('user_id', 'in', $targetUids);
|
||||
if ($month !== '' && preg_match('/^\d{4}-\d{2}$/', $month)) {
|
||||
$startTs = strtotime($month . '-01 00:00:00');
|
||||
$endTs = strtotime('+1 month', $startTs) - 1;
|
||||
if ($startTs > 0 && $endTs > 0) {
|
||||
$q->where('createtime', 'between', [$startTs, $endTs]);
|
||||
}
|
||||
}
|
||||
if ($keyword !== '') {
|
||||
$q->where(function ($sub) use ($keyword) {
|
||||
$sub->where('contact', 'like', "%{$keyword}%")
|
||||
@@ -211,6 +249,10 @@ class Merchant extends Base
|
||||
$total = $build()->count();
|
||||
$rows = $build()->order('id desc')->page($page, $limit)->select();
|
||||
$userMap = Db::name('user')->where('id', 'in', $uids)->column('id,nickname,mobile', 'id');
|
||||
$serviceIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'service_id')))));
|
||||
$serviceImgMap = $serviceIds ? Db::name('service')->where('id', 'in', $serviceIds)->column('image', 'id') : [];
|
||||
$orderIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'id')))));
|
||||
$batchMap = $orderIds ? Db::name('merchant_order')->where('order_id', 'in', $orderIds)->column('batch_no', 'order_id') : [];
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$ownerUser = (int)$row['user_id'] === (int)$merchant['user_id'];
|
||||
@@ -227,10 +269,10 @@ class Merchant extends Base
|
||||
$row['account_name'] = $u && $u['nickname'] ? $u['nickname'] : ('用户' . substr($mobile, -4));
|
||||
$row['account_role'] = $ownerUser ? 'owner' : 'member';
|
||||
$row['account_role_text'] = $ownerUser ? '主账号' : '关联账号';
|
||||
$row['batch_no'] = isset($batchMap[(int)$row['id']]) ? (string)$batchMap[(int)$row['id']] : '';
|
||||
$img = !empty($row['service_image']) ? $row['service_image'] : '';
|
||||
if (empty($img) && !empty($row['service_id'])) {
|
||||
$s = Db::name('service')->where('id', (int)$row['service_id'])->find();
|
||||
$img = $s ? $s['image'] : '';
|
||||
if (empty($img) && !empty($row['service_id']) && isset($serviceImgMap[(int)$row['service_id']])) {
|
||||
$img = $serviceImgMap[(int)$row['service_id']];
|
||||
}
|
||||
if ($img && strpos($img, 'http') !== 0) {
|
||||
$img = cdnurl($img, true);
|
||||
@@ -241,171 +283,63 @@ class Merchant extends Base
|
||||
unset($row);
|
||||
|
||||
$this->success('', [
|
||||
'list' => $rows,
|
||||
'total' => (int)$total,
|
||||
'page' => $page,
|
||||
'list' => $rows,
|
||||
'total' => (int)$total,
|
||||
'page' => $page,
|
||||
'has_more' => ($page * $limit) < (int)$total,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票记录与可开票额度(个人用户与认证商家通用)
|
||||
* 开票记录与可开票额度(个人用户与认证商家通用,兼容旧版调用并复用 InvoiceService)
|
||||
*/
|
||||
public function invoices()
|
||||
{
|
||||
$merchant = $this->merchantInfo();
|
||||
$isPassMerchant = ($merchant && $merchant['verify_status'] === 'pass');
|
||||
|
||||
if ($isPassMerchant) {
|
||||
$uids = $this->merchantUserIds($merchant);
|
||||
$mid = (int)$merchant['id'];
|
||||
$list = Db::name('invoice')
|
||||
->where(function ($q) use ($mid, $uids) {
|
||||
$q->where('merchant_id', $mid)->whereOr('user_id', 'in', $uids);
|
||||
})
|
||||
->order('id desc')
|
||||
->limit(30)
|
||||
->select();
|
||||
$paid = (float)Db::name('order')->where('user_id', 'in', $uids)->where('pay_status', 1)->sum('pay_amount');
|
||||
$used = (float)Db::name('invoice')
|
||||
->where(function ($q) use ($mid, $uids) {
|
||||
$q->where('merchant_id', $mid)->whereOr('user_id', 'in', $uids);
|
||||
})
|
||||
->where('status', 'in', ['wait', 'issued'])
|
||||
->sum('amount');
|
||||
$issued = (float)Db::name('invoice')
|
||||
->where(function ($q) use ($mid, $uids) {
|
||||
$q->where('merchant_id', $mid)->whereOr('user_id', 'in', $uids);
|
||||
})
|
||||
->where('status', 'issued')
|
||||
->sum('amount');
|
||||
} else {
|
||||
$uid = $this->uid();
|
||||
$list = Db::name('invoice')->where('user_id', $uid)->order('id desc')->limit(30)->select();
|
||||
$paid = (float)Db::name('order')->where('user_id', $uid)->where('pay_status', 1)->sum('pay_amount');
|
||||
$used = (float)Db::name('invoice')->where('user_id', $uid)->where('status', 'in', ['wait', 'issued'])->sum('amount');
|
||||
$issued = (float)Db::name('invoice')->where('user_id', $uid)->where('status', 'issued')->sum('amount');
|
||||
}
|
||||
|
||||
$statusText = ['wait' => '开票中', 'issued' => '已开票', 'reject' => '已驳回'];
|
||||
foreach ($list as &$row) {
|
||||
$row['amount'] = (float)$row['amount'];
|
||||
$row['status_text'] = isset($statusText[$row['status']]) ? $statusText[$row['status']] : $row['status'];
|
||||
$row['type_text'] = $row['type'] === 'special' ? '专用发票' : '普通发票';
|
||||
$row['title_type'] = !empty($row['tax_no']) ? 'enterprise' : 'personal';
|
||||
$row['title_type_text'] = !empty($row['tax_no']) ? '企业单位' : '个人/非企业';
|
||||
$row['time_text'] = date('Y-m-d H:i', (int)$row['createtime']);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$userRow = Db::name('user')->where('id', $this->uid())->field('nickname,mobile')->find();
|
||||
$personalDefaultTitle = ($userRow && !empty($userRow['nickname'])) ? (string)$userRow['nickname'] : '个人';
|
||||
$records = InvoiceService::getRecordList($this->uid(), 'all', 1, 30);
|
||||
$quota = InvoiceService::getQuotaSummary($this->uid());
|
||||
|
||||
$this->success('', [
|
||||
'list' => $list,
|
||||
'quota' => [
|
||||
'available' => round(max(0, $paid - $used), 2),
|
||||
'paid_amount' => round($paid, 2),
|
||||
'used_amount' => round($used, 2),
|
||||
'issued_amount' => round($issued, 2),
|
||||
'is_merchant' => $isPassMerchant,
|
||||
'default_title_type' => $isPassMerchant ? 'enterprise' : 'personal',
|
||||
'default_title' => $isPassMerchant ? ($merchant['invoice_title'] ?: $merchant['name']) : $personalDefaultTitle,
|
||||
'default_tax_no' => $isPassMerchant ? (string)$merchant['invoice_tax_no'] : '',
|
||||
'enterprise_title' => $merchant ? (string)($merchant['invoice_title'] ?: $merchant['name']) : '',
|
||||
'enterprise_tax_no' => $merchant ? (string)$merchant['invoice_tax_no'] : '',
|
||||
'personal_title' => $personalDefaultTitle,
|
||||
],
|
||||
'list' => $records['list'],
|
||||
'quota' => $quota,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请开票(支持个人用户与认证商家)
|
||||
* 申请开票(向下兼容旧版入口,底层复用 InvoiceService)
|
||||
* @ApiMethod (POST)
|
||||
*/
|
||||
public function invoice()
|
||||
{
|
||||
$merchant = $this->merchantInfo();
|
||||
$isPassMerchant = ($merchant && $merchant['verify_status'] === 'pass');
|
||||
$params = [
|
||||
'order_ids' => $this->request->post('order_ids', '', null),
|
||||
'amount' => $this->request->post('amount', 0),
|
||||
'title_id' => $this->request->post('title_id', 0),
|
||||
'type' => $this->request->post('type', 'normal'),
|
||||
'title_type' => $this->request->post('title_type', ''),
|
||||
'title' => $this->request->post('title', ''),
|
||||
'tax_no' => $this->request->post('tax_no', ''),
|
||||
'bank_name' => $this->request->post('bank_name', ''),
|
||||
'bank_account' => $this->request->post('bank_account', ''),
|
||||
'reg_address' => $this->request->post('reg_address', ''),
|
||||
'reg_mobile' => $this->request->post('reg_mobile', ''),
|
||||
'email' => $this->request->post('email', ''),
|
||||
'receive_mobile' => $this->request->post('receive_mobile', ''),
|
||||
'remark' => $this->request->post('remark', ''),
|
||||
'save_title' => $this->request->post('save_title', 0),
|
||||
];
|
||||
|
||||
$amount = round((float)$this->request->post('amount'), 2);
|
||||
$type = $this->request->post('type', 'normal');
|
||||
$titleType = trim((string)$this->request->post('title_type', ''));
|
||||
$title = trim((string)$this->request->post('title', ''));
|
||||
$taxNo = strtoupper(trim((string)$this->request->post('tax_no', '')));
|
||||
$email = trim((string)$this->request->post('email', ''));
|
||||
$remark = trim((string)$this->request->post('remark', ''));
|
||||
|
||||
if ($amount <= 0) {
|
||||
$this->error('请填写开票金额');
|
||||
}
|
||||
if (!in_array($type, ['normal', 'special'], true)) {
|
||||
$this->error('发票类型错误');
|
||||
try {
|
||||
$result = InvoiceService::submitInvoice($this->uid(), $params, false);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->error($e->getMessage());
|
||||
return;
|
||||
} catch (\Exception $e) {
|
||||
\think\Log::error('[Merchant::invoice] ' . $e->getMessage());
|
||||
$this->error('开票申请提交失败,请稍后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($titleType, ['personal', 'enterprise'], true)) {
|
||||
$titleType = ($isPassMerchant || $taxNo !== '' || $type === 'special') ? 'enterprise' : 'personal';
|
||||
}
|
||||
|
||||
if ($titleType === 'personal') {
|
||||
if ($type === 'special') {
|
||||
$this->error('个人抬头仅支持开具普通发票');
|
||||
}
|
||||
if ($title === '') {
|
||||
$userRow = Db::name('user')->where('id', $this->uid())->field('nickname')->find();
|
||||
$title = ($userRow && !empty($userRow['nickname'])) ? (string)$userRow['nickname'] : '个人';
|
||||
}
|
||||
$taxNo = '';
|
||||
} else {
|
||||
if ($title === '' && $isPassMerchant) {
|
||||
$title = (string)($merchant['invoice_title'] ?: $merchant['name']);
|
||||
}
|
||||
if ($taxNo === '' && $isPassMerchant) {
|
||||
$taxNo = strtoupper(trim((string)$merchant['invoice_tax_no']));
|
||||
}
|
||||
if ($title === '') {
|
||||
$this->error('请填写企业发票抬头');
|
||||
}
|
||||
if ($taxNo === '') {
|
||||
$this->error('请填写纳税人识别号(税号)');
|
||||
}
|
||||
}
|
||||
|
||||
if ($isPassMerchant) {
|
||||
$uids = $this->merchantUserIds($merchant);
|
||||
$mid = (int)$merchant['id'];
|
||||
$paid = (float)Db::name('order')->where('user_id', 'in', $uids)->where('pay_status', 1)->sum('pay_amount');
|
||||
$used = (float)Db::name('invoice')
|
||||
->where(function ($q) use ($mid, $uids) {
|
||||
$q->where('merchant_id', $mid)->whereOr('user_id', 'in', $uids);
|
||||
})
|
||||
->where('status', 'in', ['wait', 'issued'])
|
||||
->sum('amount');
|
||||
} else {
|
||||
$uid = $this->uid();
|
||||
$paid = (float)Db::name('order')->where('user_id', $uid)->where('pay_status', 1)->sum('pay_amount');
|
||||
$used = (float)Db::name('invoice')->where('user_id', $uid)->where('status', 'in', ['wait', 'issued'])->sum('amount');
|
||||
}
|
||||
|
||||
$available = round(max(0, $paid - $used), 2);
|
||||
if ($amount > $available) {
|
||||
$this->error('可开票金额不足,当前可开 ¥' . $available);
|
||||
}
|
||||
|
||||
Db::name('invoice')->insert([
|
||||
'merchant_id' => $isPassMerchant ? (int)$merchant['id'] : 0,
|
||||
'user_id' => $this->uid(),
|
||||
'amount' => $amount,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'tax_no' => $taxNo,
|
||||
'email' => $email,
|
||||
'remark' => $remark,
|
||||
'status' => 'wait',
|
||||
'createtime' => time(),
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
|
||||
$this->success('开票申请已提交,3 个工作日内开具');
|
||||
$this->success('开票申请已提交,3 个工作日内开具', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,7 +437,9 @@ class Merchant extends Base
|
||||
|
||||
try {
|
||||
$this->notify('user', (int)$user['id'], '已加入商家账号', '您的账号已关联到商家「' . $merchant['name'] . '」,下单、账单、开票数据将实时同步', 'system', 0);
|
||||
} catch (\Exception $e) {}
|
||||
} catch (\Exception $e) {
|
||||
\think\Log::error('[Merchant::bindMember notify] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->success('绑定成功,关联账号数据将实时同步', [
|
||||
'member_id' => (int)$id,
|
||||
@@ -542,7 +478,9 @@ class Merchant extends Base
|
||||
Db::name('merchant_member')->where('id', (int)$row['id'])->delete();
|
||||
try {
|
||||
$this->notify('user', (int)$row['user_id'], '商家关联已解除', '您已与商家「' . $merchant['name'] . '」解除关联,对方商家数据不再同步', 'system', 0);
|
||||
} catch (\Exception $e) {}
|
||||
} catch (\Exception $e) {
|
||||
\think\Log::error('[Merchant::unbindMember notify] ' . $e->getMessage());
|
||||
}
|
||||
$this->success('已解除关联,对方将不再同步商家数据');
|
||||
}
|
||||
|
||||
@@ -604,94 +542,406 @@ class Merchant extends Base
|
||||
public function importBatch()
|
||||
{
|
||||
$merchant = $this->requireMerchant();
|
||||
$items = $this->request->post('items/a', []);
|
||||
if (empty($items)) {
|
||||
$items = $this->request->post('orders/a', []);
|
||||
$rawOrders = $this->request->post('orders', null, null);
|
||||
if ($rawOrders === null || $rawOrders === '' || $rawOrders === []) {
|
||||
$rawOrders = $this->request->post('items', [], null);
|
||||
}
|
||||
|
||||
$items = [];
|
||||
if (is_string($rawOrders)) {
|
||||
$decoded = json_decode(htmlspecialchars_decode(trim($rawOrders)), true);
|
||||
if (is_array($decoded)) {
|
||||
$items = $decoded;
|
||||
}
|
||||
} elseif (is_array($rawOrders)) {
|
||||
if (count($rawOrders) === 1 && isset($rawOrders[0]) && is_string($rawOrders[0])) {
|
||||
$decoded = json_decode(htmlspecialchars_decode(trim($rawOrders[0])), true);
|
||||
$items = is_array($decoded) ? $decoded : $rawOrders;
|
||||
} else {
|
||||
$items = $rawOrders;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($items) || !is_array($items)) {
|
||||
$this->error('导入数据不能为空');
|
||||
}
|
||||
if (count($items) > 100) {
|
||||
$this->error('单次批量导入最多支持 100 条订单');
|
||||
}
|
||||
|
||||
$globalServiceId = (int)$this->request->post('service_id', 1);
|
||||
$svc = Db::name('service')->where('id', $globalServiceId)->find();
|
||||
$defaultServiceName = $svc ? $svc['name'] : '家居安装';
|
||||
$defaultPrice = $svc && (float)$svc['base_price'] > 0 ? (float)$svc['base_price'] : 80.00;
|
||||
$globalServiceId = (int)$this->request->post('service_id', 0);
|
||||
$serviceCache = [];
|
||||
$preparedRows = [];
|
||||
|
||||
foreach ($items as $idx => $item) {
|
||||
if (is_string($item)) {
|
||||
$decItem = json_decode(htmlspecialchars_decode(trim($item)), true);
|
||||
if (is_array($decItem)) {
|
||||
$item = $decItem;
|
||||
}
|
||||
}
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contact = trim((string)(isset($item['contact']) ? $item['contact'] : '买家'));
|
||||
$mobile = preg_replace('/[^\d]/', '', trim((string)(isset($item['mobile']) ? $item['mobile'] : '')));
|
||||
$address = trim((string)(isset($item['address']) ? $item['address'] : ''));
|
||||
if ($mobile === '' && $address === '') {
|
||||
continue;
|
||||
}
|
||||
$rowNo = $idx + 1;
|
||||
if (!preg_match('/^1\d{10}$/', $mobile)) {
|
||||
$this->error("第 {$rowNo} 条订单联系电话格式不正确");
|
||||
}
|
||||
if ($address === '') {
|
||||
$this->error("第 {$rowNo} 条订单缺少详细服务地址");
|
||||
}
|
||||
|
||||
$itemServiceId = !empty($item['service_id']) ? (int)$item['service_id'] : $globalServiceId;
|
||||
if ($itemServiceId <= 0) {
|
||||
$this->error("第 {$rowNo} 条订单未指定有效的服务项目");
|
||||
}
|
||||
if (!array_key_exists($itemServiceId, $serviceCache)) {
|
||||
$serviceCache[$itemServiceId] = Db::name('service')
|
||||
->where('id', $itemServiceId)
|
||||
->where('status', 'normal')
|
||||
->find() ?: null;
|
||||
}
|
||||
$svc = $serviceCache[$itemServiceId];
|
||||
if (!$svc) {
|
||||
$this->error("第 {$rowNo} 条订单关联的服务项目不存在或已下架");
|
||||
}
|
||||
if ($svc['price_type'] === 'quote') {
|
||||
$this->error("第 {$rowNo} 条订单所选服务「{$svc['name']}」为师傅报价项目,不支持批量一口价导入");
|
||||
}
|
||||
|
||||
$goodsNum = max(1, (int)(isset($item['goods_num']) ? $item['goods_num'] : (isset($item['num']) ? $item['num'] : 1)));
|
||||
$floor = isset($item['floor']) ? max(1, (int)$item['floor']) : 1;
|
||||
$hasElevator = isset($item['has_elevator'])
|
||||
? (int)$item['has_elevator']
|
||||
: (isset($item['elevator']) ? (int)$item['elevator'] : 1);
|
||||
$urgent = !empty($item['urgent']) ? 1 : 0;
|
||||
$specId = isset($item['spec_id']) ? (int)$item['spec_id'] : 0;
|
||||
$reqServiceType = isset($item['service_type']) ? trim((string)$item['service_type']) : '';
|
||||
|
||||
// 严禁采信前端传入的 price,强制根据数据库 fa_service / fa_service_spec 与楼层规则计算真实金额
|
||||
try {
|
||||
$calc = Home::calcAmount($svc, [
|
||||
'floor' => $floor,
|
||||
'has_elevator' => $hasElevator ? 1 : 0,
|
||||
'urgent' => $urgent,
|
||||
'goods_num' => $goodsNum,
|
||||
'spec_id' => $specId,
|
||||
'service_type' => $reqServiceType,
|
||||
'strict' => true,
|
||||
], 0, $this->uid(), 'fixed', 0, []);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->error("第 {$rowNo} 条订单计价校验失败:" . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if ((float)$calc['pay_amount'] <= 0) {
|
||||
$this->error("第 {$rowNo} 条订单计算金额异常");
|
||||
}
|
||||
|
||||
$preparedRows[] = [
|
||||
'svc' => $svc,
|
||||
'service_id' => (int)$svc['id'],
|
||||
'cat_id' => (int)$svc['cat_id'],
|
||||
'service_name' => (string)$svc['name'],
|
||||
'service_img' => (string)$svc['image'],
|
||||
'goods_model' => mb_substr(trim((string)(isset($item['goods_model']) ? $item['goods_model'] : '')), 0, 100),
|
||||
'goods_num' => $goodsNum,
|
||||
'contact' => mb_substr($contact ?: '买家', 0, 50),
|
||||
'mobile' => $mobile,
|
||||
'province' => mb_substr(trim((string)(isset($item['province']) && $item['province'] !== '' ? $item['province'] : ($merchant['province'] ?: '贵州省'))), 0, 50),
|
||||
'city' => mb_substr(trim((string)(isset($item['city']) && $item['city'] !== '' ? $item['city'] : ($merchant['city'] ?: '贵阳市'))), 0, 50),
|
||||
'district' => mb_substr(trim((string)(isset($item['district']) ? $item['district'] : ($merchant['district'] ?: ''))), 0, 50),
|
||||
'address' => mb_substr($address, 0, 255),
|
||||
'floor' => $floor,
|
||||
'has_elevator' => $hasElevator ? 1 : 0,
|
||||
'urgent' => $urgent,
|
||||
'remark' => mb_substr(trim((string)(isset($item['remark']) ? $item['remark'] : '')), 0, 500),
|
||||
'appoint_time' => !empty($item['appoint_time']) ? (int)$item['appoint_time'] : null,
|
||||
'calc' => $calc,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($preparedRows)) {
|
||||
$this->error('未解析到有效的导入订单数据');
|
||||
}
|
||||
|
||||
$ordersCreated = [];
|
||||
$orderIds = [];
|
||||
$totalAmount = 0.00;
|
||||
$isMonthly = false;
|
||||
$batchNo = 'MB' . date('YmdHis') . mt_rand(1000, 9999);
|
||||
$now = time();
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$batchNo = 'MB' . date('YmdHis') . mt_rand(1000, 9999);
|
||||
foreach ($items as $idx => $item) {
|
||||
$contact = trim((string)(isset($item['contact']) ? $item['contact'] : '买家'));
|
||||
$mobile = trim((string)(isset($item['mobile']) ? $item['mobile'] : ''));
|
||||
$address = trim((string)(isset($item['address']) ? $item['address'] : ''));
|
||||
$serviceName = trim((string)(isset($item['service_name']) ? $item['service_name'] : $defaultServiceName));
|
||||
$goodsModel = trim((string)(isset($item['goods_model']) ? $item['goods_model'] : ''));
|
||||
$goodsNum = max(1, (int)(isset($item['goods_num']) ? $item['goods_num'] : (isset($item['num']) ? $item['num'] : 1)));
|
||||
$lockedMerchant = Db::name('merchant')->where('id', (int)$merchant['id'])->lock(true)->find();
|
||||
if (!$lockedMerchant || $lockedMerchant['verify_status'] !== 'pass') {
|
||||
throw new \InvalidArgumentException('商家资质状态异常,无法批量发单');
|
||||
}
|
||||
$isMonthly = ($lockedMerchant['settle_type'] === 'monthly');
|
||||
$initStatus = $isMonthly ? OrderStatus::PENDING_ACCEPT : OrderStatus::PENDING_PAY;
|
||||
$initPayStatus = $isMonthly ? 1 : 0;
|
||||
$initPayType = $isMonthly ? 'monthly' : '';
|
||||
$initPayTime = $isMonthly ? $now : null;
|
||||
|
||||
if (!$mobile || !$address) {
|
||||
continue;
|
||||
}
|
||||
foreach ($preparedRows as $pRow) {
|
||||
$orderNo = $this->makeOrderNo();
|
||||
$verifyCode = str_pad((string)mt_rand(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
$calc = $pRow['calc'];
|
||||
$payAmt = round((float)$calc['pay_amount'], 2);
|
||||
$unitPrice = round((float)$calc['base_amount'] / max(1, (int)$pRow['goods_num']), 2);
|
||||
|
||||
$orderNo = date('YmdHis') . mt_rand(100000, 999999);
|
||||
$singlePrice = isset($item['price']) && (float)$item['price'] > 0 ? (float)$item['price'] : $defaultPrice;
|
||||
$baseAmount = round($singlePrice * $goodsNum, 2);
|
||||
$verifyCode = (string)mt_rand(100000, 999999);
|
||||
|
||||
$orderId = Db::name('order')->insertGetId([
|
||||
$orderId = (int)Db::name('order')->insertGetId([
|
||||
'order_no' => $orderNo,
|
||||
'user_id' => $this->uid(),
|
||||
'service_id' => $globalServiceId,
|
||||
'service_name' => $serviceName,
|
||||
'goods_model' => $goodsModel,
|
||||
'goods_num' => $goodsNum,
|
||||
'contact' => $contact,
|
||||
'mobile' => $mobile,
|
||||
'address' => $address,
|
||||
'province' => isset($item['province']) ? $item['province'] : '贵州省',
|
||||
'city' => isset($item['city']) ? $item['city'] : '贵阳市',
|
||||
'district' => isset($item['district']) ? $item['district'] : '南明区',
|
||||
'floor' => isset($item['floor']) ? (int)$item['floor'] : 1,
|
||||
'has_elevator' => isset($item['elevator']) ? (int)$item['elevator'] : 1,
|
||||
'base_amount' => $baseAmount,
|
||||
'total_amount' => $baseAmount,
|
||||
'pay_amount' => $baseAmount,
|
||||
'status' => OrderStatus::PENDING_ACCEPT,
|
||||
'pay_status' => 1,
|
||||
'pay_time' => time(),
|
||||
'master_id' => 0,
|
||||
'cat_id' => $pRow['cat_id'],
|
||||
'service_id' => $pRow['service_id'],
|
||||
'service_name' => $pRow['service_name'],
|
||||
'service_image' => $pRow['service_img'],
|
||||
'price_type' => 'fixed',
|
||||
'goods_model' => $pRow['goods_model'],
|
||||
'goods_num' => $pRow['goods_num'],
|
||||
'contact' => $pRow['contact'],
|
||||
'mobile' => $pRow['mobile'],
|
||||
'province' => $pRow['province'],
|
||||
'city' => $pRow['city'],
|
||||
'district' => $pRow['district'],
|
||||
'address' => $pRow['address'],
|
||||
'floor' => $pRow['floor'],
|
||||
'has_elevator' => $pRow['has_elevator'],
|
||||
'urgent' => $pRow['urgent'],
|
||||
'remark' => $pRow['remark'],
|
||||
'appoint_time' => $pRow['appoint_time'],
|
||||
'base_amount' => $calc['base_amount'],
|
||||
'floor_amount' => $calc['floor_amount'],
|
||||
'urgent_amount' => $calc['urgent_amount'],
|
||||
'discount_amount' => 0.00,
|
||||
'total_amount' => $calc['total_amount'],
|
||||
'pay_amount' => $payAmt,
|
||||
'status' => $initStatus,
|
||||
'pay_status' => $initPayStatus,
|
||||
'pay_type' => $initPayType,
|
||||
'pay_time' => $initPayTime,
|
||||
'transaction_id' => $isMonthly ? ('MONTHLY' . date('YmdHis', $now) . mt_rand(1000, 9999)) : '',
|
||||
'verify_code' => $verifyCode,
|
||||
'createtime' => time(),
|
||||
'updatetime' => time(),
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
|
||||
Db::name('order_goods')->insert([
|
||||
'order_id' => $orderId,
|
||||
'service_id' => $pRow['service_id'],
|
||||
'goods_name' => $pRow['service_name'],
|
||||
'goods_model' => $pRow['goods_model'],
|
||||
'goods_num' => $pRow['goods_num'],
|
||||
'goods_image' => $pRow['service_img'],
|
||||
'addon_services' => '',
|
||||
'base_price' => $unitPrice,
|
||||
'createtime' => $now,
|
||||
]);
|
||||
|
||||
Db::name('merchant_order')->insert([
|
||||
'merchant_id' => (int)$merchant['id'],
|
||||
'merchant_id' => (int)$lockedMerchant['id'],
|
||||
'order_id' => $orderId,
|
||||
'batch_no' => $batchNo,
|
||||
'createtime' => time(),
|
||||
'createtime' => $now,
|
||||
]);
|
||||
|
||||
$this->addLog(
|
||||
$orderId,
|
||||
$initStatus,
|
||||
$isMonthly ? "企业批量导单(月结授信,批次 {$batchNo}),已进入接单大厅" : "企业批量导单创建(待支付,批次 {$batchNo})",
|
||||
'user',
|
||||
$this->uid()
|
||||
);
|
||||
|
||||
$orderIds[] = $orderId;
|
||||
$ordersCreated[] = [
|
||||
'order_id' => $orderId,
|
||||
'order_no' => $orderNo,
|
||||
'contact' => $contact,
|
||||
'amount' => $baseAmount
|
||||
'contact' => $pRow['contact'],
|
||||
'amount' => $payAmt,
|
||||
];
|
||||
$totalAmount += $baseAmount;
|
||||
$totalAmount = round($totalAmount + $payAmt, 2);
|
||||
}
|
||||
|
||||
if ($isMonthly) {
|
||||
Db::name('merchant')->where('id', (int)$lockedMerchant['id'])->update([
|
||||
'order_count' => (int)$lockedMerchant['order_count'] + count($ordersCreated),
|
||||
'total_amount' => round((float)$lockedMerchant['total_amount'] + $totalAmount, 2),
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
Db::rollback();
|
||||
$this->error($e->getMessage());
|
||||
return;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
\think\Log::error('[Merchant::importBatch] ' . $e->getMessage());
|
||||
$this->error('批量导入失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->success('批量下单导入成功!', [
|
||||
'batch_no' => $batchNo,
|
||||
'count' => count($ordersCreated),
|
||||
'total_amount' => round($totalAmount, 2),
|
||||
'orders' => $ordersCreated,
|
||||
if ($isMonthly && !empty($orderIds)) {
|
||||
try {
|
||||
$masters = Db::name('master')
|
||||
->where('verify_status', 'pass')
|
||||
->where('work_status', 'on')
|
||||
->where('user_id', '<>', $this->uid())
|
||||
->limit(20)
|
||||
->select();
|
||||
$cnt = count($orderIds);
|
||||
foreach ($masters as $m) {
|
||||
$this->notify('master', (int)$m['id'], '企业批量新工单就绪', "商家刚发布了 {$cnt} 笔新工单进入接单大厅,请尽快前往查看抢单!", 'order', (int)$orderIds[0]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\think\Log::error('[Merchant::importBatch broadcast] ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->success($isMonthly ? '批量下单成功,已按月结授信进入接单大厅!' : '批量导入成功,请完成批次支付', [
|
||||
'need_pay' => !$isMonthly,
|
||||
'settle_type' => $isMonthly ? 'monthly' : 'prepay',
|
||||
'batch_no' => $batchNo,
|
||||
'count' => count($ordersCreated),
|
||||
'total_amount' => round($totalAmount, 2),
|
||||
'order_ids' => $orderIds,
|
||||
'orders' => $ordersCreated,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 商户端:预付商家按批次号支付待支付批量订单
|
||||
* @ApiMethod (POST)
|
||||
*/
|
||||
public function payBatch()
|
||||
{
|
||||
$merchant = $this->requireMerchant();
|
||||
$batchNo = trim((string)$this->request->post('batch_no', ''));
|
||||
$payType = trim((string)$this->request->post('pay_type', 'wechat'));
|
||||
if ($payType === '') {
|
||||
$payType = 'mock';
|
||||
}
|
||||
if ($batchNo === '') {
|
||||
$this->error('缺少批次号 batch_no');
|
||||
}
|
||||
|
||||
$batchLinks = Db::name('merchant_order')->where('batch_no', $batchNo)->select();
|
||||
if (empty($batchLinks)) {
|
||||
$this->error('批次订单不存在');
|
||||
}
|
||||
$batchOrderIds = [];
|
||||
foreach ($batchLinks as $link) {
|
||||
if ((int)$link['merchant_id'] !== (int)$merchant['id']) {
|
||||
$this->error('无权操作该批次订单', null, 403);
|
||||
}
|
||||
$oid = (int)$link['order_id'];
|
||||
if ($oid > 0) {
|
||||
$batchOrderIds[] = $oid;
|
||||
}
|
||||
}
|
||||
$batchOrderIds = array_values(array_unique($batchOrderIds));
|
||||
if (empty($batchOrderIds)) {
|
||||
$this->error('该批次下无有效订单');
|
||||
}
|
||||
|
||||
$paidCount = 0;
|
||||
$paidTotal = 0.00;
|
||||
$paidOrderIds = [];
|
||||
$now = time();
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$lockedMerchant = Db::name('merchant')->where('id', (int)$merchant['id'])->lock(true)->find();
|
||||
$orders = Db::name('order')
|
||||
->where('id', 'in', $batchOrderIds)
|
||||
->lock(true)
|
||||
->select();
|
||||
|
||||
foreach ($orders as $ord) {
|
||||
if ((int)$ord['pay_status'] === 1 || (int)$ord['status'] !== OrderStatus::PENDING_PAY) {
|
||||
continue;
|
||||
}
|
||||
$oid = (int)$ord['id'];
|
||||
$amt = round((float)$ord['pay_amount'], 2);
|
||||
$txId = 'BATCH' . date('YmdHis', $now) . mt_rand(1000, 9999);
|
||||
|
||||
Db::name('order')->where('id', $oid)->update([
|
||||
'status' => OrderStatus::PENDING_ACCEPT,
|
||||
'pay_status' => 1,
|
||||
'pay_type' => $payType,
|
||||
'pay_time' => $now,
|
||||
'transaction_id' => $txId,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
|
||||
$this->addLog($oid, OrderStatus::PENDING_ACCEPT, "企业批次支付完成(批次 {$batchNo}),订单已进入接单大厅", 'user', $this->uid());
|
||||
$paidCount++;
|
||||
$paidTotal = round($paidTotal + $amt, 2);
|
||||
$paidOrderIds[] = $oid;
|
||||
}
|
||||
|
||||
if ($paidCount === 0) {
|
||||
throw new \InvalidArgumentException('该批次没有待支付的订单,请勿重复支付');
|
||||
}
|
||||
|
||||
if ($lockedMerchant) {
|
||||
Db::name('merchant')->where('id', (int)$lockedMerchant['id'])->update([
|
||||
'order_count' => (int)$lockedMerchant['order_count'] + $paidCount,
|
||||
'total_amount' => round((float)$lockedMerchant['total_amount'] + $paidTotal, 2),
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
Db::rollback();
|
||||
$this->error($e->getMessage());
|
||||
return;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
\think\Log::error('[Merchant::payBatch] ' . $e->getMessage());
|
||||
$this->error('批次支付失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$masters = Db::name('master')
|
||||
->where('verify_status', 'pass')
|
||||
->where('work_status', 'on')
|
||||
->where('user_id', '<>', $this->uid())
|
||||
->limit(20)
|
||||
->select();
|
||||
foreach ($masters as $m) {
|
||||
$this->notify('master', (int)$m['id'], '企业批量新工单就绪', "同城有 {$paidCount} 笔企业批量工单进入接单大厅,请尽快前往查看抢单!", 'order', (int)$paidOrderIds[0]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\think\Log::error('[Merchant::payBatch broadcast] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->success('批次支付成功,订单已进入接单大厅', [
|
||||
'batch_no' => $batchNo,
|
||||
'count' => $paidCount,
|
||||
'paid_count' => $paidCount,
|
||||
'total_amount' => round($paidTotal, 2),
|
||||
'order_ids' => $paidOrderIds,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -741,13 +991,31 @@ class Merchant extends Base
|
||||
{
|
||||
$m = $this->merchantInfo();
|
||||
if (!$m) {
|
||||
$this->error('请先完成商家入驻', ['need_apply' => 1], 2001);
|
||||
$curUser = Db::name('user')->where('id', $this->uid())->find();
|
||||
if ($curUser && !empty($curUser['user_type']) && $curUser['user_type'] === 'merchant') {
|
||||
$now = time();
|
||||
$newId = Db::name('merchant')->insertGetId([
|
||||
'user_id' => $this->uid(),
|
||||
'name' => !empty($curUser['nickname']) ? (string)$curUser['nickname'] : '商家用户',
|
||||
'license_no' => '',
|
||||
'contact' => (string)$curUser['nickname'],
|
||||
'mobile' => (string)$curUser['mobile'],
|
||||
'address' => isset($curUser['contact_address']) ? (string)$curUser['contact_address'] : '',
|
||||
'email' => isset($curUser['email']) ? (string)$curUser['email'] : '',
|
||||
'verify_status' => 'pass',
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
return Db::name('merchant')->where('id', $newId)->find();
|
||||
}
|
||||
$this->error('请先在「账号信息」切换为商家类型', null, 2001);
|
||||
}
|
||||
if ($m['verify_status'] === 'pending') {
|
||||
$this->error('商家资质审核中,请耐心等待', ['verify_status' => 'pending'], 2002);
|
||||
}
|
||||
if ($m['verify_status'] === 'reject') {
|
||||
$this->error('资质未通过:' . $m['audit_msg'], ['verify_status' => 'reject'], 2003);
|
||||
if ($m['verify_status'] !== 'pass') {
|
||||
Db::name('merchant')->where('id', (int)$m['id'])->update([
|
||||
'verify_status' => 'pass',
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
$m['verify_status'] = 'pass';
|
||||
}
|
||||
return $m;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户