微信登录修复: - 根因:manifest.json 的 mp-weixin.appid 为空 → 开发者工具用测试号,code 与服务端 appid 不匹配 - 修复:填入 wx40ca4b02d4d1cb58 - WxLogin 增加错误码语义映射(40029 appid 不一致 / 40125 secret 错 / 45011 频繁 / 40226 风控) - 登录页新增手机号登录兜底通道(微信不可用时业务不被卡住) 商家(企业账号): - 新表 fa_merchant / fa_merchant_order / fa_invoice(纯新增,DB 40→45 表) - Merchant 控制器:apply/info/update/bills/invoices/invoice/bindBatch 入驻含企业信息+营业执照+开票信息+结算方式(预付/月结) 账单按月汇总、开票额度按已付-已开票校验、批量下单按批次归属 - Auth::switchRole 支持 merchant(2001/2002/2003 语义码 + redirect) - Auth::profile 增加 is_merchant / merchant 前端: - 登录页角色扩为 业主/师傅/商家 三类 - pages/merchant/apply 企业入驻;workbench 商家工作台(经营数据+按月账单+近期订单+六宫格) - pages/merchant/batch 批量下单(多地址×多数量)→ 逐单建单支付 → 绑定批次 - pages/merchant/invoice 开票申请与记录 - pages/mine 增加商家入驻/切换商家端入口;角色徽章三态 验证:163 项断言全通过(page_flow 47 + e2e 65 + phase2 27 + phase3 24)
293 行
11 KiB
PHP
293 行
11 KiB
PHP
<?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';
|
||
}
|
||
|
||
$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;
|
||
}
|
||
|
||
$uid = $this->uid();
|
||
$orderCount = (int)Db::name('order')->where('user_id', $uid)->where('pay_status', 1)->count();
|
||
$totalAmount = (float)Db::name('order')->where('user_id', $uid)->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,
|
||
'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'],
|
||
'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();
|
||
$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()
|
||
{
|
||
$this->requireMerchant();
|
||
$uid = $this->uid();
|
||
|
||
$months = Db::name('order')
|
||
->field("FROM_UNIXTIME(createtime,'%Y-%m') as month, count(*) as cnt, sum(pay_amount) as amount")
|
||
->where('user_id', $uid)->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', $uid)->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 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', $this->uid())->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 个工作日内开具');
|
||
}
|
||
|
||
/**
|
||
* 批量下单后绑定批次(订单本身走已验收的 /order/create)
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function bindBatch()
|
||
{
|
||
$merchant = $this->requireMerchant();
|
||
// 兼容三种入参:数组 / 逗号串 / 混合
|
||
$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', $this->uid())->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()
|
||
{
|
||
return Db::name('merchant')->where('user_id', $this->uid())->find();
|
||
}
|
||
|
||
/**
|
||
* 要求已通过审核的商家
|
||
*/
|
||
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;
|
||
}
|
||
}
|