文件
yjyf/server/application/api/controller/Merchant.php
linda fbc6feee87 feat(merchant): 企业订单聚合列表与关联账号只读详情
- 新增 /merchant/orders,按主账号+关联账号聚合订单并支持状态筛选
- 新增企业订单页,展示订单归属账号(主账号/关联账号)
- 工作台企业订单入口改到聚合列表,近期订单可点击查看
- 订单详情允许同商家账号查看,非下单账号隐藏操作按钮
- 报价选择仅下单账号可操作,防止关联账号/主账号误操作
2026-10-28 19:27:51 +08:00

531 行
21 KiB
PHP

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

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

<?php
namespace app\api\controller;
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)->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');
$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),
],
]);
}
/**
* 维护资料(联系人 / 地址 / 开票信息)
* @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);
$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');
$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) {
$q = Db::name('order')->where('user_id', 'in', $uids);
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');
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'] : '';
$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 ? '主账号' : '关联账号';
}
unset($row);
$this->success('', [
'list' => $rows,
'total' => (int)$total,
'page' => $page,
]);
}
/**
* 开票记录
*/
public function invoices()
{
$merchant = $this->requireMerchant();
$list = Db::name('invoice')->where('merchant_id', $merchant['id'])->order('id desc')->limit(30)->select();
$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['time_text'] = date('Y-m-d H:i', (int)$row['createtime']);
}
unset($row);
$this->success('', ['list' => $list]);
}
/**
* 申请开票
* @ApiMethod (POST)
*/
public function invoice()
{
$merchant = $this->requireMerchant();
$amount = round((float)$this->request->post('amount'), 2);
$type = $this->request->post('type', 'normal');
$email = trim((string)$this->request->post('email'));
if ($amount <= 0) {
$this->error('请填写开票金额');
}
if (!in_array($type, ['normal', 'special'], true)) {
$this->error('发票类型错误');
}
$paid = (float)Db::name('order')->where('user_id', 'in', $this->merchantUserIds($merchant))->where('pay_status', 1)->sum('pay_amount');
$used = (float)Db::name('invoice')->where('merchant_id', $merchant['id'])->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' => (int)$merchant['id'],
'user_id' => $this->uid(),
'amount' => $amount,
'type' => $type,
'title' => $merchant['invoice_title'] ?: $merchant['name'],
'tax_no' => $merchant['invoice_tax_no'],
'email' => $email,
'status' => 'wait',
'createtime' => time(),
'updatetime' => time(),
]);
$this->success('开票申请已提交,3 个工作日内开具');
}
/**
* 商家关联账号列表(主账号 + 已绑定账号)
*/
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) {}
$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) {}
$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]);
}
/**
* 当前用户商家档案:主账号直接匹配,关联账号通过绑定关系匹配
*/
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) {
$this->error('请先完成商家入驻', ['need_apply' => 1], 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);
}
return $m;
}
}