| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- <?php
- namespace plugin\saiadmin\app\logic\user;
- use plugin\saiadmin\app\model\system\SystemUser;
- use plugin\saiadmin\app\model\user\UserFeedback;
- use plugin\saiadmin\basic\think\BaseLogic;
- use plugin\saiadmin\exception\ApiException;
- use plugin\saiadmin\service\OpenSpoutWriter;
- use support\Response;
- use support\think\Db;
- /** 用户反馈列表、详情、回复和处理状态逻辑。 */
- class UserFeedbackLogic extends BaseLogic
- {
- private const TYPE_NAMES = [1 => '订单问题', 2 => '系统问题', 3 => '其他问题'];
- private const STATUS_NAMES = [0 => '未处理', 1 => '处理中', 2 => '已处理'];
- private const DEVICE_NAMES = [
- 1 => '充电底座', 100 => 'K霸宝', 101 => '充电宝', 102 => 'K霸宝 Blue',
- 103 => '密码线充', 104 => '蓝牙线充', 105 => '密码蓝牙线充', 107 => '车载充电器',
- ];
- public function __construct()
- {
- $this->model = new UserFeedback();
- }
- public function indexList(array $filters): array
- {
- $page = max(1, (int) request()->input('page', 1));
- $limit = min(100, max(1, (int) request()->input('limit', 20)));
- $result = $this->buildQuery($filters)
- ->order('f.create_time', 'desc')
- ->order('f.id', 'desc')
- ->paginate(['page' => $page, 'list_rows' => $limit])
- ->toArray();
- $rows = $result['data'] ?? [];
- $replies = $this->latestReplies(array_map('intval', array_column($rows, 'id')));
- $result['data'] = array_map(
- fn (array $row): array => $this->formatFeedback($row, $replies[(int) $row['id']] ?? null),
- $rows
- );
- $result['summary'] = $this->summary($filters);
- return $result;
- }
- public function readFeedback(int $id): array
- {
- $row = $this->buildQuery([])->where('f.id', $id)->find();
- if (!$row) {
- throw new ApiException('反馈不存在或无权查看');
- }
- $feedback = $this->formatFeedback($row);
- $replies = Db::table('sa_user_feedback_reply')->where('feedback_id', $id)
- ->whereNull('delete_time')->order('id', 'asc')->select()->toArray();
- $feedback['replies'] = array_map(fn (array $reply): array => [
- 'id' => (int) $reply['id'],
- 'author_type' => (int) $reply['author_type'],
- 'author_name' => (string) $reply['author_name'],
- 'content' => (string) $reply['content'],
- 'images' => $this->decodeImages($reply['images'] ?? ''),
- 'create_time' => (string) $reply['create_time'],
- ], $replies);
- return $feedback;
- }
- public function updateStatus(array $data, int $operatorId, string $operatorName): void
- {
- $id = (int) ($data['id'] ?? 0);
- $this->assertAccessible($id);
- $status = (int) $data['status'];
- $now = date('Y-m-d H:i:s');
- Db::table('sa_user_feedback')->where('id', $id)->update([
- 'status' => $status,
- 'remark' => trim((string) ($data['remark'] ?? '')),
- 'handled_by' => $operatorId,
- 'handled_name' => $operatorName,
- 'handled_at' => $status === 2 ? $now : null,
- 'update_time' => $now,
- ]);
- }
- public function reply(array $data, int $operatorId, string $operatorName): array
- {
- $id = (int) ($data['id'] ?? 0);
- $feedback = $this->assertAccessible($id);
- $content = trim((string) $data['content']);
- $now = date('Y-m-d H:i:s');
- $replyId = $this->transaction(function () use ($id, $feedback, $content, $operatorId, $operatorName, $now) {
- $replyId = Db::table('sa_user_feedback_reply')->insertGetId([
- 'feedback_id' => $id,
- 'author_type' => 0,
- 'author_id' => $operatorId,
- 'author_name' => $operatorName,
- 'content' => $content,
- 'images' => '[]',
- 'create_time' => $now,
- 'update_time' => $now,
- ]);
- $update = [
- 'handled_by' => $operatorId,
- 'handled_name' => $operatorName,
- 'update_time' => $now,
- ];
- if ((int) ($feedback['status'] ?? 0) === 0) {
- $update['status'] = 1;
- }
- Db::table('sa_user_feedback')->where('id', $id)->update($update);
- return $replyId;
- });
- return ['id' => (int) $replyId];
- }
- public function export(array $filters): Response
- {
- $rows = $this->buildQuery($filters)->order('f.create_time', 'desc')->order('f.id', 'desc')
- ->select()->toArray();
- $latest = $this->latestReplies(array_map('intval', array_column($rows, 'id')));
- $data = [];
- foreach ($rows as $row) {
- $item = $this->formatFeedback($row, $latest[(int) $row['id']] ?? null);
- $data[] = [
- 'id' => $item['id'],
- 'identity' => $this->excelSafe($item['identity']),
- 'nickname' => $this->excelSafe($item['nickname']),
- 'phone' => $this->excelSafe($item['phone']),
- 'type_name' => $item['type_name'],
- 'merchant_name' => $this->excelSafe($item['merchant_name']),
- 'device_sn' => $this->excelSafe($item['device_sn']),
- 'device_type_name' => $item['device_type_name'],
- 'content' => $this->excelSafe($item['content']),
- 'status_name' => $item['status_name'],
- 'latest_reply' => $this->excelSafe($item['latest_reply']),
- 'remark' => $this->excelSafe($item['remark']),
- 'create_time' => $item['create_time'],
- ];
- }
- $fileName = '用户反馈_' . date('Ymd_His') . '.xlsx';
- $writer = new OpenSpoutWriter($fileName);
- $writer->setWidth([10, 12, 18, 16, 14, 22, 22, 16, 40, 12, 35, 30, 20]);
- $writer->setHeader([
- '反馈ID', '身份', '用户昵称', '联系电话', '问题类型', '商户', '设备SN',
- '设备类型', '反馈内容', '处理状态', '最新回复', '备注', '反馈时间',
- ]);
- $writer->setData($data);
- return response()->download($writer->returnFile(), urlencode($fileName));
- }
- private function buildQuery(array $filters)
- {
- $query = Db::table('sa_user_feedback')->alias('f')
- ->leftJoin('sa_rental_user u', 'u.id = f.user_id AND u.delete_time IS NULL')
- ->leftJoin('sa_rental_order o', 'o.id = f.order_id AND o.delete_time IS NULL')
- ->leftJoin('sa_device d', 'd.id = f.device_id AND d.delete_time IS NULL')
- ->leftJoin('sa_merchant m', 'm.id = f.merchant_id AND m.delete_time IS NULL')
- ->whereNull('f.delete_time')
- ->field(implode(',', [
- 'f.id,f.agent_id,f.user_id,f.identity,f.type,f.order_id,f.merchant_id,f.merchant_name',
- 'f.device_id,f.device_sn,f.device_type,f.nickname,f.contact_phone,f.content,f.images',
- 'f.status,f.remark,f.handled_by,f.handled_name,f.handled_at,f.create_time,f.update_time',
- 'u.nickname AS user_nickname,u.phone AS user_phone',
- 'o.order_sn,o.transaction_id',
- 'd.goods_sn AS related_device_sn,d.device_code,d.goods_type AS related_device_type',
- 'm.name AS related_merchant_name',
- ]));
- $managerIds = $this->accessibleManagerIds();
- if (is_array($managerIds)) {
- if ($managerIds === []) {
- $query->where('f.id', 0);
- } else {
- $query->whereIn('f.agent_id', $managerIds);
- }
- }
- $status = trim((string) ($filters['status'] ?? ''));
- if (in_array($status, ['0', '1', '2'], true)) {
- $query->where('f.status', (int) $status);
- }
- $type = trim((string) ($filters['type'] ?? ''));
- if (in_array($type, ['1', '2', '3'], true)) {
- $query->where('f.type', (int) $type);
- }
- $deviceType = trim((string) ($filters['device_type'] ?? ''));
- if ($deviceType !== '' && ctype_digit($deviceType)) {
- $query->where('f.device_type', (int) $deviceType);
- }
- $this->like($query, 'f.contact_phone', $filters['phone'] ?? '');
- $this->like($query, 'f.merchant_name', $filters['merchant'] ?? '');
- $this->like($query, 'f.device_sn', $filters['device_sn'] ?? '');
- $this->like($query, 'f.content', $filters['content'] ?? '');
- $startDate = trim((string) ($filters['start_date'] ?? ''));
- $endDate = trim((string) ($filters['end_date'] ?? ''));
- if ($startDate !== '') {
- $query->where('f.create_time', '>=', $startDate . ' 00:00:00');
- }
- if ($endDate !== '') {
- $query->where('f.create_time', '<=', $endDate . ' 23:59:59');
- }
- return $query;
- }
- private function summary(array $filters): array
- {
- $summaryFilters = $filters;
- $summaryFilters['status'] = '';
- $rows = $this->buildQuery($summaryFilters)->field('f.status,COUNT(*) AS total')
- ->group('f.status')->select()->toArray();
- $summary = ['all' => 0, 'pending' => 0, 'processing' => 0, 'done' => 0];
- foreach ($rows as $row) {
- $count = (int) $row['total'];
- $summary['all'] += $count;
- if ((int) $row['status'] === 0) $summary['pending'] = $count;
- if ((int) $row['status'] === 1) $summary['processing'] = $count;
- if ((int) $row['status'] === 2) $summary['done'] = $count;
- }
- return $summary;
- }
- /** @return array<int,array> */
- private function latestReplies(array $feedbackIds): array
- {
- if ($feedbackIds === []) {
- return [];
- }
- $ids = Db::table('sa_user_feedback_reply')->whereIn('feedback_id', $feedbackIds)
- ->whereNull('delete_time')->field('MAX(id) AS id')->group('feedback_id')->column('id');
- if ($ids === []) {
- return [];
- }
- $rows = Db::table('sa_user_feedback_reply')->whereIn('id', $ids)->select()->toArray();
- $map = [];
- foreach ($rows as $row) {
- $map[(int) $row['feedback_id']] = $row;
- }
- return $map;
- }
- private function formatFeedback(array $row, ?array $latestReply = null): array
- {
- $deviceType = (int) (($row['device_type'] ?? 0) ?: ($row['related_device_type'] ?? 0));
- $status = (int) ($row['status'] ?? 0);
- $type = (int) ($row['type'] ?? 3);
- return [
- 'id' => (int) $row['id'],
- 'agent_id' => (int) ($row['agent_id'] ?? 0),
- 'user_id' => (int) ($row['user_id'] ?? 0),
- 'identity' => (string) (($row['identity'] ?? '') ?: '用户'),
- 'nickname' => (string) (($row['nickname'] ?? '') ?: ($row['user_nickname'] ?? '匿名用户')),
- 'phone' => (string) (($row['contact_phone'] ?? '') ?: ($row['user_phone'] ?? '')),
- 'type' => $type,
- 'type_name' => self::TYPE_NAMES[$type] ?? '其他问题',
- 'order_id' => (int) ($row['order_id'] ?? 0),
- 'order_sn' => (string) ($row['order_sn'] ?? ''),
- 'transaction_id' => (string) ($row['transaction_id'] ?? ''),
- 'merchant_id' => (int) ($row['merchant_id'] ?? 0),
- 'merchant_name' => (string) (($row['merchant_name'] ?? '') ?: ($row['related_merchant_name'] ?? '')),
- 'device_id' => (int) ($row['device_id'] ?? 0),
- 'device_sn' => (string) (($row['device_sn'] ?? '') ?: ($row['related_device_sn'] ?? '')),
- 'device_code' => (string) ($row['device_code'] ?? ''),
- 'device_type' => $deviceType,
- 'device_type_name' => self::DEVICE_NAMES[$deviceType] ?? ($deviceType > 0 ? '其他设备' : '--'),
- 'content' => (string) ($row['content'] ?? ''),
- 'images' => $this->decodeImages($row['images'] ?? ''),
- 'status' => $status,
- 'status_name' => self::STATUS_NAMES[$status] ?? '未知',
- 'remark' => (string) ($row['remark'] ?? ''),
- 'handled_by' => (int) ($row['handled_by'] ?? 0),
- 'handled_name' => (string) ($row['handled_name'] ?? ''),
- 'handled_at' => (string) ($row['handled_at'] ?? ''),
- 'latest_reply_identity' => (string) ($latestReply['author_name'] ?? ''),
- 'latest_reply' => (string) ($latestReply['content'] ?? ''),
- 'create_time' => (string) ($row['create_time'] ?? ''),
- 'update_time' => (string) ($row['update_time'] ?? ''),
- ];
- }
- private function assertAccessible(int $id): array
- {
- if ($id <= 0) {
- throw new ApiException('反馈参数错误');
- }
- $query = Db::table('sa_user_feedback')->where('id', $id)->whereNull('delete_time');
- $managerIds = $this->accessibleManagerIds();
- if (is_array($managerIds)) {
- if ($managerIds === []) {
- throw new ApiException('反馈不存在或无权操作');
- }
- $query->whereIn('agent_id', $managerIds);
- }
- $feedback = $query->find();
- if (!$feedback) {
- throw new ApiException('反馈不存在或无权操作');
- }
- return $feedback;
- }
- /** @return int[]|null */
- private function accessibleManagerIds(): ?array
- {
- $currentId = (int) ($this->adminInfo['id'] ?? 0);
- if ($currentId === 1 || (int) ($this->adminInfo['is_super'] ?? 0) === 1) {
- return null;
- }
- if ($currentId <= 0) {
- return [];
- }
- $managerIds = [$currentId];
- $dept = $this->adminInfo['deptList'] ?? [];
- if (empty($dept['id']) || !isset($dept['level'])) {
- return $managerIds;
- }
- $prefix = (string) $dept['level'] . (int) $dept['id'] . ',';
- $deptIds = Db::table('sa_system_dept')->whereNull('delete_time')
- ->whereLike('level', $prefix . '%')->column('id');
- if ($deptIds !== []) {
- $managerIds = array_merge($managerIds, array_map('intval', SystemUser::whereIn('dept_id', $deptIds)
- ->whereNull('delete_time')->column('id')));
- }
- return array_values(array_unique($managerIds));
- }
- private function decodeImages(mixed $images): array
- {
- if (is_array($images)) {
- return array_values(array_filter(array_map('strval', $images)));
- }
- $decoded = json_decode((string) $images, true);
- return is_array($decoded) ? array_values(array_filter(array_map('strval', $decoded))) : [];
- }
- private function like($query, string $field, mixed $value): void
- {
- $value = trim((string) $value);
- if ($value !== '') {
- $query->whereLike($field, '%' . $value . '%');
- }
- }
- private function excelSafe(mixed $value): string
- {
- $value = (string) $value;
- return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value;
- }
- }
|