文件

932 行
41 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\api\controller;
use app\common\library\CommissionService;
use app\common\library\FinanceConfigService;
use app\common\library\InvoiceService;
use app\common\library\OrderStatus;
use think\Db;
/**
* 商家端(企业账号):入驻 / 资料 / 账单 / 开票 / 批量下单归属
*/
class Merchant extends Base
{
/**
* 商家入驻 / 重新提交
* @ApiMethod (POST)
*/
public function apply()
{
$data = [
'name' => trim((string)$this->request->post('name')),
'license_no' => trim((string)$this->request->post('license_no')),
'license_image' => (string)$this->request->post('license_image'),
'legal_person' => trim((string)$this->request->post('legal_person')),
'contact' => trim((string)$this->request->post('contact')),
'mobile' => trim((string)$this->request->post('mobile')),
'province' => trim((string)$this->request->post('province')),
'city' => trim((string)$this->request->post('city')),
'district' => trim((string)$this->request->post('district')),
'address' => trim((string)$this->request->post('address')),
'invoice_title' => trim((string)$this->request->post('invoice_title')),
'invoice_tax_no' => trim((string)$this->request->post('invoice_tax_no')),
'invoice_bank' => trim((string)$this->request->post('invoice_bank')),
'invoice_account' => trim((string)$this->request->post('invoice_account')),
'settle_type' => $this->request->post('settle_type', 'prepay'),
];
if ($data['name'] === '' || $data['license_no'] === '' || $data['contact'] === '' || $data['mobile'] === '') {
$this->error('请填写企业名称、统一社会信用代码、联系人和电话');
}
if (!preg_match('/^1\d{10}$/', $data['mobile'])) {
$this->error('联系电话格式不正确');
}
if (!in_array($data['settle_type'], ['prepay', 'monthly'], true)) {
$data['settle_type'] = 'prepay';
}
// 已被商家绑定的关联账号,企业资料由主账号维护
if (Db::name('merchant_member')->where('user_id', $this->uid())->find()) {
$this->error('当前账号已绑定商家,企业资料请由主账号维护');
}
$exist = Db::name('merchant')->where('user_id', $this->uid())->find();
if ($exist && $exist['verify_status'] === 'pass') {
$this->error('您已是认证商家,无需重复入驻');
}
$save = array_merge($data, [
'verify_status' => 'pending',
'audit_msg' => '',
'updatetime' => time(),
]);
if ($exist) {
Db::name('merchant')->where('id', $exist['id'])->update($save);
$id = (int)$exist['id'];
} else {
$save['user_id'] = $this->uid();
$save['createtime'] = time();
$id = Db::name('merchant')->insertGetId($save);
}
$this->success('提交成功,请等待审核', ['merchant_id' => $id]);
}
/**
* 商家档案与经营数据
*/
public function info()
{
$merchant = $this->merchantInfo();
if (!$merchant) {
$this->success('未入驻', ['applied' => false]);
return;
}
$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)->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'],
'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),
],
]);
}
/**
* 维护资料(联系人 / 地址 / 开票信息)
* @ApiMethod (POST)
*/
public function update()
{
$merchant = $this->requireMerchant();
$this->requireMerchantOwner($merchant);
$fields = ['contact', 'mobile', 'province', 'city', 'district', 'address',
'invoice_title', 'invoice_tax_no', 'invoice_bank', 'invoice_account'];
$data = ['updatetime' => time()];
foreach ($fields as $f) {
$v = $this->request->post($f, null);
if ($v !== null) {
$data[$f] = trim((string)$v);
}
}
if (isset($data['mobile']) && $data['mobile'] !== '' && !preg_match('/^1\d{10}$/', $data['mobile'])) {
$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('已保存');
}
/**
* 账单中心:按月汇总 + 近期订单
*/
public function bills()
{
$merchant = $this->requireMerchant();
$uids = $this->merchantUserIds($merchant);
$months = Db::name('order')
->field("FROM_UNIXTIME(createtime,'%Y-%m') as month, count(*) as cnt, sum(pay_amount) as amount")
->where('user_id', 'in', $uids)->where('pay_status', 1)
->group('month')->order('month desc')->limit(12)->select();
foreach ($months as &$m) {
$m['amount'] = round((float)$m['amount'], 2);
$m['cnt'] = (int)$m['cnt'];
}
unset($m);
$orders = Db::name('order')->field('id,order_no,service_name,pay_amount,status,createtime')
->where('user_id', 'in', $uids)->order('id desc')->limit(20)->select();
foreach ($orders as &$o) {
$o['pay_amount'] = (float)$o['pay_amount'];
$o['status_text'] = \app\common\library\OrderStatus::label($o['status']);
$o['time_text'] = date('Y-m-d H:i', (int)$o['createtime']);
}
unset($o);
$this->success('', ['months' => $months, 'orders' => $orders]);
}
/**
* 企业订单:主账号 + 关联账号的全部订单
*/
public function orders()
{
$merchant = $this->requireMerchant();
$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)));
$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}%")
->whereOr('mobile', 'like', "%{$keyword}%")
->whereOr('order_no', 'like', "%{$keyword}%")
->whereOr('service_name', 'like', "%{$keyword}%")
->whereOr('address', 'like', "%{$keyword}%")
->whereOr('remark', 'like', "%{$keyword}%");
});
}
if ($status === 'active') {
$q->where('status', 'in', \app\common\library\OrderStatus::activeValues());
} elseif ($status === 'completed') {
$q->where('status', \app\common\library\OrderStatus::COMPLETED);
} elseif ($status === 'cancelled') {
$q->where('status', \app\common\library\OrderStatus::CANCELLED);
} elseif ($status !== 'all' && $status !== '') {
$q->where('status', (int)$status);
}
return $q;
};
$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'];
$u = isset($userMap[(int)$row['user_id']]) ? $userMap[(int)$row['user_id']] : null;
$mobile = $u ? (string)$u['mobile'] : '';
$custMobile = (string)$row['mobile'];
$row['mobile_mask'] = strlen($custMobile) === 11 ? substr_replace($custMobile, '****', 3, 4) : $custMobile;
$row['pay_amount'] = (float)$row['pay_amount'];
$row['urgent'] = (int)$row['urgent'];
$row['status_text'] = \app\common\library\OrderStatus::label((int)$row['status']);
$row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门';
$row['create_text'] = date('Y-m-d H:i', (int)$row['createtime']);
$row['address_text'] = $row['province'] . $row['city'] . $row['district'] . $row['address'];
$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']) && isset($serviceImgMap[(int)$row['service_id']])) {
$img = $serviceImgMap[(int)$row['service_id']];
}
if ($img && strpos($img, 'http') !== 0) {
$img = cdnurl($img, true);
}
$row['service_image'] = $img;
$row['goods_image_url'] = $img;
}
unset($row);
$this->success('', [
'list' => $rows,
'total' => (int)$total,
'page' => $page,
'has_more' => ($page * $limit) < (int)$total,
]);
}
/**
* 开票记录与可开票额度(个人用户与认证商家通用,兼容旧版调用并复用 InvoiceService)
*/
public function invoices()
{
$records = InvoiceService::getRecordList($this->uid(), 'all', 1, 30);
$quota = InvoiceService::getQuotaSummary($this->uid());
$this->success('', [
'list' => $records['list'],
'quota' => $quota,
]);
}
/**
* 申请开票(向下兼容旧版入口,底层复用 InvoiceService)
* @ApiMethod (POST)
*/
public function invoice()
{
$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),
];
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;
}
$this->success('开票申请已提交,3 个工作日内开具', $result);
}
/**
* 商家关联账号列表(主账号 + 已绑定账号)
*/
public function members()
{
$merchant = $this->requireMerchant();
$uids = $this->merchantUserIds($merchant);
$isOwner = (int)$merchant['user_id'] === $this->uid();
$users = Db::name('user')->where('id', 'in', $uids)->column('id,nickname,mobile,avatar', 'id');
$bindRows = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id'])->select();
$bindMap = [];
foreach ($bindRows as $b) {
$bindMap[(int)$b['user_id']] = $b;
}
$stats = Db::name('order')->field('user_id,count(*) as cnt,sum(pay_amount) as amount')
->where('user_id', 'in', $uids)->where('pay_status', 1)
->group('user_id')->select();
$statMap = [];
foreach ($stats as $s) {
$statMap[(int)$s['user_id']] = $s;
}
$list = [];
foreach ($uids as $uid) {
$u = isset($users[$uid]) ? $users[$uid] : null;
$owner = (int)$uid === (int)$merchant['user_id'];
$bind = isset($bindMap[(int)$uid]) ? $bindMap[(int)$uid] : null;
$mobile = $u ? (string)$u['mobile'] : '';
$list[] = [
'id' => $owner ? 0 : (int)$bind['id'],
'user_id' => (int)$uid,
'nickname' => $u ? ($u['nickname'] ?: ('用户' . substr($mobile, -4))) : '用户',
'mobile' => $mobile !== '' ? substr_replace($mobile, '****', 3, 4) : '',
'avatar' => $u ? $u['avatar'] : '',
'role' => $owner ? 'owner' : 'member',
'role_text' => $owner ? '主账号' : '关联账号',
'is_owner' => $owner,
'order_count' => isset($statMap[(int)$uid]) ? (int)$statMap[(int)$uid]['cnt'] : 0,
'total_amount' => isset($statMap[(int)$uid]) ? round((float)$statMap[(int)$uid]['amount'], 2) : 0,
'bind_time_text' => (!$owner && $bind) ? date('Y-m-d H:i', (int)$bind['createtime']) : '',
];
}
// 主账号排在最前
usort($list, function ($a, $b) {
return (int)$b['is_owner'] - (int)$a['is_owner'];
});
$this->success('', [
'list' => $list,
'is_owner' => $isOwner,
'total' => count($list),
]);
}
/**
* 绑定关联账号(仅主账号)
* @ApiMethod (POST)
*/
public function bindMember()
{
$merchant = $this->requireMerchant();
$this->requireMerchantOwner($merchant);
$mobile = trim((string)$this->request->post('mobile'));
if (!preg_match('/^1\d{10}$/', $mobile)) {
$this->error('请输入正确的关联账号手机号');
}
$user = Db::name('user')->where('mobile', $mobile)->find();
if (!$user) {
$this->error('该手机号尚未注册,请对方先登录小程序');
}
if ((int)$user['id'] === $this->uid()) {
$this->error('不能绑定当前主账号');
}
if (Db::name('merchant')->where('user_id', $user['id'])->find()) {
$this->error('该账号已是商家主账号,不能重复绑定');
}
if (Db::name('merchant_member')->where('user_id', $user['id'])->find()) {
$this->error('该账号已绑定其他商家');
}
$id = Db::name('merchant_member')->insertGetId([
'merchant_id' => (int)$merchant['id'],
'user_id' => (int)$user['id'],
'remark' => '',
'createtime' => time(),
'updatetime' => time(),
]);
try {
$this->notify('user', (int)$user['id'], '已加入商家账号', '您的账号已关联到商家「' . $merchant['name'] . '」,下单、账单、开票数据将实时同步', 'system', 0);
} catch (\Exception $e) {
\think\Log::error('[Merchant::bindMember notify] ' . $e->getMessage());
}
$this->success('绑定成功,关联账号数据将实时同步', [
'member_id' => (int)$id,
'user_id' => (int)$user['id'],
'nickname' => $user['nickname'] ?: ('用户' . substr($mobile, -4)),
]);
}
/**
* 解除关联账号(仅主账号)
* @ApiMethod (POST)
*/
public function unbindMember()
{
$merchant = $this->requireMerchant();
$this->requireMerchantOwner($merchant);
$memberId = (int)$this->request->post('member_id');
$userId = (int)$this->request->post('user_id');
$query = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id']);
if ($memberId > 0) {
$query->where('id', $memberId);
} elseif ($userId > 0) {
$query->where('user_id', $userId);
} else {
$this->error('缺少关联账号参数');
}
$row = $query->find();
if (!$row) {
$this->error('关联账号不存在或已解除');
}
if ((int)$row['user_id'] === (int)$merchant['user_id']) {
$this->error('不能解除主账号');
}
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) {
\think\Log::error('[Merchant::unbindMember notify] ' . $e->getMessage());
}
$this->success('已解除关联,对方将不再同步商家数据');
}
/**
* 批量下单后绑定批次(订单本身走已验收的 /order/create)
* @ApiMethod (POST)
*/
public function bindBatch()
{
$merchant = $this->requireMerchant();
$uids = $this->merchantUserIds($merchant);
// 兼容三种入参:数组 / 逗号串 / 混合
$raw = $this->request->post('order_ids');
$orderIds = [];
foreach ((array)$raw as $v) {
foreach (explode(',', (string)$v) as $one) {
$one = (int)$one;
if ($one > 0) {
$orderIds[] = $one;
}
}
}
$orderIds = array_values(array_unique($orderIds));
$batchNo = trim((string)$this->request->post('batch_no', ''));
if (empty($orderIds)) {
$this->error('缺少订单');
}
if ($batchNo === '') {
$batchNo = date('YmdHis') . mt_rand(100, 999);
}
$ok = 0;
foreach ($orderIds as $oid) {
$oid = (int)$oid;
if (!Db::name('order')->where('id', $oid)->where('user_id', 'in', $uids)->find()) {
continue;
}
if (Db::name('merchant_order')->where('order_id', $oid)->find()) {
continue;
}
Db::name('merchant_order')->insert([
'merchant_id' => (int)$merchant['id'],
'order_id' => $oid,
'batch_no' => $batchNo,
'createtime' => time(),
]);
$ok++;
}
$this->success('已归属到批次', ['batch_no' => $batchNo, 'count' => $ok]);
}
/**
* 当前用户商家档案:主账号直接匹配,关联账号通过绑定关系匹配
*/
/**
* 商户端:Excel 或结构化数据一键批量解析下单
* @ApiMethod (POST)
*/
public function importBatch()
{
$merchant = $this->requireMerchant();
$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', 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} 条订单计算金额异常");
}
$resolvedProvince = mb_substr(trim((string)(isset($item['province']) && $item['province'] !== '' ? $item['province'] : ($merchant['province'] ?: ''))), 0, 50);
$resolvedCity = mb_substr(trim((string)(isset($item['city']) && $item['city'] !== '' ? $item['city'] : ($merchant['city'] ?: ''))), 0, 50);
if ($resolvedProvince === '' || $resolvedCity === '') {
$this->error("第 {$rowNo} 条订单缺少服务省份或城市信息,请在Excel或表格中填写后再提交");
}
$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' => $resolvedProvince,
'city' => $resolvedCity,
'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();
// 事务外一次性预加载生效抽佣规则与默认兜底配置,循环内纯内存匹配快照(零额外 SQL)
$activeCommRules = CommissionService::getActiveRules();
$commDefaults = FinanceConfigService::getCommissionDefaults();
Db::startTrans();
try {
$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;
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);
$commMatch = CommissionService::matchOrderSnapshot([
'price_type' => 'fixed',
'customer_type' => 'merchant',
'cat_id' => (int)$pRow['cat_id'],
'urgent' => (int)$pRow['urgent'],
'order_amount' => $payAmt,
], $activeCommRules, $commDefaults);
$orderId = (int)Db::name('order')->insertGetId([
'order_no' => $orderNo,
'user_id' => $this->uid(),
'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,
'commission_rate' => $commMatch['commission_rate'],
'commission_snapshot' => $commMatch['commission_snapshot'],
'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' => $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)$lockedMerchant['id'],
'order_id' => $orderId,
'batch_no' => $batchNo,
'createtime' => $now,
]);
$this->addLog(
$orderId,
$initStatus,
$isMonthly ? "企业批量导单(月结授信,批次 {$batchNo}),已进入接单大厅" : "企业批量导单创建(待支付,批次 {$batchNo})",
'user',
$this->uid()
);
$orderIds[] = $orderId;
$ordersCreated[] = [
'order_id' => $orderId,
'order_no' => $orderNo,
'contact' => $pRow['contact'],
'amount' => $payAmt,
];
$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;
}
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()
{
$this->requireMerchant();
$this->error('系统已全面禁用模拟支付通道,企业预付工单请逐笔使用真实微信支付完成付款(月结授信企业导单时已直接授信进入大厅)');
}
protected function merchantInfo()
{
$merchant = Db::name('merchant')->where('user_id', $this->uid())->find();
if ($merchant) {
return $merchant;
}
$bind = Db::name('merchant_member')->where('user_id', $this->uid())->find();
if ($bind) {
return Db::name('merchant')->where('id', (int)$bind['merchant_id'])->find();
}
return null;
}
/**
* 商家数据归属用户集合:主账号 + 所有关联账号
*/
protected function merchantUserIds($merchant)
{
$ids = [(int)$merchant['user_id']];
$rows = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id'])->column('user_id');
foreach ($rows as $id) {
$id = (int)$id;
if ($id > 0 && !in_array($id, $ids, true)) {
$ids[] = $id;
}
}
return $ids;
}
/**
* 仅商家主账号可执行敏感操作
*/
protected function requireMerchantOwner($merchant)
{
if ((int)$merchant['user_id'] !== $this->uid()) {
$this->error('仅主账号可执行此操作', null, 403);
}
}
/**
* 要求已通过审核的商家
*/
protected function requireMerchant()
{
$m = $this->merchantInfo();
if (!$m) {
$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'] !== 'pass') {
Db::name('merchant')->where('id', (int)$m['id'])->update([
'verify_status' => 'pass',
'updatetime' => time(),
]);
$m['verify_status'] = 'pass';
}
return $m;
}
}