feat: 新增发票抬头、晒单、钱包日志等模型及后台管理功能
- 新增 invoice_title、showcase、wallet_log 模型及对应语言包 - 新增后台控制器、视图及搜索字段配置 - 修复服务类目删除保护逻辑 - 优化后台登录页面文案及样式 - 新增前端角色切换逻辑及页面拦截 - 新增 SVG 图标及样式变量 - 修复接口权限及下拉框默认值问题
这个提交包含在:
@@ -3,30 +3,32 @@
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\InvoiceService;
|
||||
use think\Db;
|
||||
use think\exception\PDOException;
|
||||
|
||||
/**
|
||||
* 开票申请
|
||||
* 开票申请管理
|
||||
*
|
||||
* 开票申请仅由用户/商户端发起,后台只负责财务开具处理:
|
||||
* 待开具(wait) → 已开票(issued,必填发票号)/驳回(reject,必填原因)
|
||||
* 驳回后额度自动释放,并通知申请人
|
||||
* 开票申请仅由用户/商户端发起,后台负责财务审核与开具交付:
|
||||
* 待开具(wait) → 已开票(issued,必填发票号,支持上传电子发票PDF/图片,锁定关联订单为已开票)
|
||||
* → 驳回(reject,必填原因,自动释放开票额度并解锁关联订单)
|
||||
*
|
||||
* @icon fa fa-file-text-o
|
||||
*/
|
||||
class Invoice extends Backend
|
||||
{
|
||||
|
||||
/**
|
||||
* Invoice模型对象
|
||||
* @var \app\admin\model\Invoice
|
||||
*/
|
||||
protected $model = null;
|
||||
protected $relationSearch = true;
|
||||
protected $searchFields = 'id,invoice_sn,title,tax_no,invoice_no,email,merchant.name,user.nickname';
|
||||
|
||||
/**
|
||||
* 开票处理状态机:当前状态 => 允许流转到的目标状态
|
||||
* issued/reject 为终态
|
||||
* issued / reject / cancel 为终态
|
||||
* @var array
|
||||
*/
|
||||
protected $statusFlow = [
|
||||
@@ -38,6 +40,7 @@ class Invoice extends Backend
|
||||
parent::_initialize();
|
||||
$this->model = new \app\admin\model\Invoice;
|
||||
$this->view->assign("typeList", $this->model->getTypeList());
|
||||
$this->view->assign("titleTypeList", $this->model->getTitleTypeList());
|
||||
$this->view->assign("statusList", $this->model->getStatusList());
|
||||
}
|
||||
|
||||
@@ -64,29 +67,46 @@ class Invoice extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务开票处理:申请信息只读,仅允许填写开票结果
|
||||
* - 开票完成:必须填写发票号,状态置为已开票
|
||||
* - 驳回:必须填写原因,额度自动释放
|
||||
* 详情回看(复用 edit 只读视图)
|
||||
*/
|
||||
public function detail($ids = null)
|
||||
{
|
||||
$row = $this->model->with(['user', 'merchant'])->where('invoice.id', (int)$ids)->find();
|
||||
if (!$row) {
|
||||
$this->error(__('No Results were found'));
|
||||
}
|
||||
$linkedOrders = InvoiceService::fetchLinkedOrders((string)$row['order_ids'], (int)$row['id']);
|
||||
$this->view->assign('row', $row);
|
||||
$this->view->assign('linkedOrders', $linkedOrders);
|
||||
$this->view->assign('flowOptions', []);
|
||||
return $this->view->fetch('edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务开票处理:申请信息只读,仅允许填写开票结果与上传电子发票文件
|
||||
*/
|
||||
public function edit($ids = null)
|
||||
{
|
||||
$row = $this->model->get($ids);
|
||||
$row = $this->model->with(['user', 'merchant'])->where('invoice.id', (int)$ids)->find();
|
||||
if (!$row) {
|
||||
$this->error(__('No Results were found'));
|
||||
}
|
||||
if (false === $this->request->isPost()) {
|
||||
$linkedOrders = InvoiceService::fetchLinkedOrders((string)$row['order_ids'], (int)$row['id']);
|
||||
$this->view->assign('row', $row);
|
||||
$this->view->assign('linkedOrders', $linkedOrders);
|
||||
$this->view->assign('flowOptions', $this->statusFlow[$row['status']] ?? []);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
$status = (string)$this->request->post('row.status');
|
||||
$invoiceNo = trim((string)$this->request->post('row.invoice_no'));
|
||||
$auditMsg = trim((string)$this->request->post('row.audit_msg'));
|
||||
$status = (string)$this->request->post('row.status');
|
||||
$invoiceCode = trim((string)$this->request->post('row.invoice_code'));
|
||||
$invoiceNo = trim((string)$this->request->post('row.invoice_no'));
|
||||
$invoiceFile = trim((string)$this->request->post('row.invoice_file'));
|
||||
$auditMsg = trim((string)$this->request->post('row.audit_msg'));
|
||||
|
||||
// 状态机校验:终态禁止操作,流转目标必须合法
|
||||
if (!isset($this->statusFlow[$row['status']])) {
|
||||
$this->error('该开票申请已处理完毕,不允许再次变更');
|
||||
$this->error('该开票申请已处理或已撤回,不允许再次变更');
|
||||
}
|
||||
if (!in_array($status, $this->statusFlow[$row['status']], true)) {
|
||||
$this->error('状态流转不合法,请刷新后重试');
|
||||
@@ -98,30 +118,49 @@ class Invoice extends Backend
|
||||
$this->error('驳回时必须填写驳回原因');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
Db::startTrans();
|
||||
try {
|
||||
$updateData = [
|
||||
'status' => $status,
|
||||
'invoice_code' => $invoiceCode !== '' ? mb_substr($invoiceCode, 0, 50) : $row['invoice_code'],
|
||||
'invoice_no' => $invoiceNo !== '' ? mb_substr($invoiceNo, 0, 50) : $row['invoice_no'],
|
||||
'invoice_file' => $invoiceFile !== '' ? mb_substr($invoiceFile, 0, 500) : $row['invoice_file'],
|
||||
'audit_msg' => mb_substr($auditMsg, 0, 255),
|
||||
'updatetime' => $now,
|
||||
];
|
||||
if ($status === 'issued') {
|
||||
$updateData['issue_time'] = $now;
|
||||
}
|
||||
|
||||
// 乐观锁更新状态,防止并发重复处理
|
||||
$affected = Db::name('invoice')
|
||||
->where('id', $row['id'])
|
||||
->where('status', $row['status'])
|
||||
->update([
|
||||
'status' => $status,
|
||||
'invoice_no' => $invoiceNo !== '' ? $invoiceNo : $row['invoice_no'],
|
||||
'audit_msg' => $auditMsg,
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
->update($updateData);
|
||||
if (!$affected) {
|
||||
throw new \Exception('开票申请状态已被其他人变更,请刷新后重试');
|
||||
}
|
||||
|
||||
// 通知申请人(商户开票通知绑定账号用户)
|
||||
$this->sendUserMessage(
|
||||
(int)$row['user_id'],
|
||||
$status === 'issued' ? '发票已开具' : '开票申请已驳回',
|
||||
$status === 'issued'
|
||||
? '您申请的开票 ¥' . $row['amount'] . ' 已开具完成,发票号码:' . $invoiceNo . ',电子发票已发送至 ' . $row['email']
|
||||
: '您的开票申请 ¥' . $row['amount'] . ' 未通过审核:' . ($auditMsg !== '' ? $auditMsg : '无')
|
||||
);
|
||||
if ($status === 'issued') {
|
||||
InvoiceService::markInvoiceOrdersIssued((int)$row['id'], (string)$row['order_ids'], $now);
|
||||
$msgBody = '您申请的发票 ¥' . number_format((float)$row['amount'], 2, '.', '') . ' 已开具完成,发票号码:' . $invoiceNo;
|
||||
if ($invoiceFile !== '') {
|
||||
$msgBody .= ',您可在「开具发票 - 开票记录」中在线预览或下载电子发票';
|
||||
}
|
||||
if (!empty($row['email'])) {
|
||||
$msgBody .= '(接收邮箱:' . $row['email'] . ')';
|
||||
}
|
||||
$this->sendUserMessage((int)$row['user_id'], '发票已开具', $msgBody);
|
||||
} else {
|
||||
InvoiceService::releaseInvoiceOrders((int)$row['id'], (string)$row['order_ids'], $now);
|
||||
$this->sendUserMessage(
|
||||
(int)$row['user_id'],
|
||||
'开票申请已驳回',
|
||||
'您的开票申请 ¥' . number_format((float)$row['amount'], 2, '.', '') . ' 未通过审核:' . ($auditMsg !== '' ? $auditMsg : '请核对开票信息后重新提交')
|
||||
);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (PDOException|\Exception $e) {
|
||||
Db::rollback();
|
||||
@@ -156,9 +195,6 @@ class Invoice extends Backend
|
||||
|
||||
/**
|
||||
* 向申请人发送站内消息
|
||||
* @param int $userId 用户ID
|
||||
* @param string $title 消息标题
|
||||
* @param string $content 消息内容
|
||||
*/
|
||||
protected function sendUserMessage($userId, $title, $content)
|
||||
{
|
||||
@@ -167,7 +203,7 @@ class Invoice extends Backend
|
||||
}
|
||||
Db::name('message')->insert([
|
||||
'receive_type' => 'user',
|
||||
'receive_id' => $userId,
|
||||
'receive_id' => (int)$userId,
|
||||
'type' => 'system',
|
||||
'title' => mb_substr((string)$title, 0, 100),
|
||||
'content' => mb_substr((string)$content, 0, 500),
|
||||
|
||||
在新工单中引用
屏蔽一个用户