UserComplaintLogic.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <?php
  2. namespace plugin\saiadmin\app\logic\user;
  3. use DateTimeImmutable;
  4. use plugin\saiadmin\app\service\user\WechatPayComplaintService;
  5. use plugin\saiadmin\basic\think\BaseLogic;
  6. use plugin\saiadmin\exception\ApiException;
  7. use support\think\Db;
  8. /** 微信支付用户投诉查询与处理。 */
  9. class UserComplaintLogic extends BaseLogic
  10. {
  11. private WechatPayComplaintService $wechat;
  12. public function __construct(?WechatPayComplaintService $wechat = null)
  13. {
  14. $this->wechat = $wechat ?? new WechatPayComplaintService();
  15. }
  16. public function configOptions(): array
  17. {
  18. return $this->configQuery()
  19. ->field('id,partner_id,mini_program_name,appid,mchid')
  20. ->order('id', 'asc')
  21. ->select()
  22. ->toArray();
  23. }
  24. public function indexList(array $filters): array
  25. {
  26. $config = $this->resolveConfig((int) ($filters['config_id'] ?? 0));
  27. [$startDate, $endDate] = $this->dateRange(
  28. (string) ($filters['start_date'] ?? ''),
  29. (string) ($filters['end_date'] ?? '')
  30. );
  31. $page = max(1, (int) request()->input('page', 1));
  32. $limit = min(50, max(1, (int) request()->input('limit', 20)));
  33. $result = $this->wechat->complaints($config, [
  34. 'begin_date' => $startDate,
  35. 'end_date' => $endDate,
  36. 'limit' => $limit,
  37. 'offset' => ($page - 1) * $limit,
  38. ]);
  39. $rows = array_map(fn (array $row): array => $this->formatComplaint($row), $result['data'] ?? []);
  40. $this->appendRefundRemarks($rows);
  41. return [
  42. 'data' => $rows,
  43. 'total' => (int) ($result['total_count'] ?? count($rows)),
  44. 'current_page' => $page,
  45. 'per_page' => $limit,
  46. 'config' => $this->publicConfig($config),
  47. ];
  48. }
  49. public function detail(int $configId, string $complaintId): array
  50. {
  51. $complaintId = $this->complaintId($complaintId);
  52. $config = $this->resolveConfig($configId);
  53. $result = $this->wechat->detail($config, $complaintId);
  54. if (!empty($result['payer_phone'])) {
  55. $result['payer_phone'] = $this->wechat->decryptSensitive(
  56. (string) $result['payer_phone'],
  57. (string) $config['apiclient_key']
  58. );
  59. }
  60. return $this->formatComplaint($result);
  61. }
  62. public function history(int $configId, string $complaintId): array
  63. {
  64. $config = $this->resolveConfig($configId);
  65. $result = $this->wechat->history($config, $this->complaintId($complaintId));
  66. return array_map(function (array $item): array {
  67. $item['complaint_media_list'] = array_values((array) ($item['complaint_media_list'] ?? []));
  68. return $item;
  69. }, $result['data'] ?? []);
  70. }
  71. public function reply(array $data): array
  72. {
  73. $content = trim((string) ($data['content'] ?? ''));
  74. if ($content === '' || mb_strlen($content) > 200) {
  75. throw new ApiException('回复内容不能为空且不能超过 200 个字');
  76. }
  77. $config = $this->resolveConfig((int) ($data['config_id'] ?? 0));
  78. $this->wechat->reply($config, $this->complaintId((string) ($data['complaint_id'] ?? '')), $content);
  79. return [];
  80. }
  81. public function complete(array $data): array
  82. {
  83. $config = $this->resolveConfig((int) ($data['config_id'] ?? 0));
  84. $this->wechat->complete($config, $this->complaintId((string) ($data['complaint_id'] ?? '')));
  85. return [];
  86. }
  87. private function configQuery()
  88. {
  89. $query = Db::table('sa_wechat_mini_program')->whereNull('delete_time');
  90. if (!$this->isSuperAdmin()) {
  91. $query->where('partner_id', $this->currentAdminId());
  92. }
  93. return $query;
  94. }
  95. private function resolveConfig(int $configId): array
  96. {
  97. $query = $this->configQuery();
  98. if ($configId > 0) {
  99. $query->where('id', $configId);
  100. }
  101. $config = $query->order('id', 'asc')->find();
  102. if (!$config) {
  103. throw new ApiException($configId > 0 ? '微信小程序配置不存在或无权访问' : '请先绑定微信小程序并完善微信支付配置');
  104. }
  105. return $config;
  106. }
  107. /** @return array{0:string,1:string} */
  108. private function dateRange(string $startDate, string $endDate): array
  109. {
  110. if ($startDate === '' || $endDate === '') {
  111. throw new ApiException('请选择投诉查询日期范围');
  112. }
  113. $start = DateTimeImmutable::createFromFormat('!Y-m-d', $startDate);
  114. $end = DateTimeImmutable::createFromFormat('!Y-m-d', $endDate);
  115. if (!$start || !$end || $start->format('Y-m-d') !== $startDate || $end->format('Y-m-d') !== $endDate) {
  116. throw new ApiException('投诉查询日期格式错误');
  117. }
  118. if ($start > $end) {
  119. throw new ApiException('开始日期不能晚于结束日期');
  120. }
  121. if ((int) $start->diff($end)->days > 30) {
  122. throw new ApiException('单次最多查询连续 31 天的投诉记录');
  123. }
  124. return [$startDate, $endDate];
  125. }
  126. private function complaintId(string $complaintId): string
  127. {
  128. $complaintId = trim($complaintId);
  129. if ($complaintId === '' || mb_strlen($complaintId) > 64) {
  130. throw new ApiException('投诉单号错误');
  131. }
  132. return $complaintId;
  133. }
  134. private function formatComplaint(array $row): array
  135. {
  136. $orders = [];
  137. foreach ((array) ($row['complaint_order_info'] ?? []) as $order) {
  138. if (!is_array($order)) {
  139. continue;
  140. }
  141. if (isset($order['amount'])) {
  142. $order['amount'] = round(((float) $order['amount']) / 100, 2);
  143. }
  144. $orders[] = $order;
  145. }
  146. $row['complaint_order_info'] = $orders;
  147. $row['complaint_media_list'] = array_values((array) ($row['complaint_media_list'] ?? []));
  148. $row['refund_remark'] = (string) ($row['refund_remark'] ?? '');
  149. return $row;
  150. }
  151. private function appendRefundRemarks(array &$rows): void
  152. {
  153. $transactionIds = [];
  154. foreach ($rows as $row) {
  155. foreach ($row['complaint_order_info'] ?? [] as $order) {
  156. if (!empty($order['transaction_id'])) {
  157. $transactionIds[] = (string) $order['transaction_id'];
  158. }
  159. }
  160. }
  161. $transactionIds = array_values(array_unique($transactionIds));
  162. if ($transactionIds === []) {
  163. return;
  164. }
  165. $orders = Db::table('sa_rental_order')->whereIn('transaction_id', $transactionIds)
  166. ->whereNull('delete_time')->field('transaction_id,refund_amount,remark')->select()->toArray();
  167. $map = [];
  168. foreach ($orders as $order) {
  169. if ((float) ($order['refund_amount'] ?? 0) > 0 || trim((string) ($order['remark'] ?? '')) !== '') {
  170. $map[(string) $order['transaction_id']] = (string) (($order['remark'] ?? '') ?: '已退款');
  171. }
  172. }
  173. foreach ($rows as &$row) {
  174. foreach ($row['complaint_order_info'] ?? [] as $order) {
  175. $transactionId = (string) ($order['transaction_id'] ?? '');
  176. if (isset($map[$transactionId])) {
  177. $row['refund_remark'] = $map[$transactionId];
  178. break;
  179. }
  180. }
  181. }
  182. unset($row);
  183. }
  184. private function publicConfig(array $config): array
  185. {
  186. return [
  187. 'id' => (int) $config['id'],
  188. 'mini_program_name' => (string) $config['mini_program_name'],
  189. 'appid' => (string) $config['appid'],
  190. 'mchid' => (string) $config['mchid'],
  191. ];
  192. }
  193. private function currentAdminId(): int
  194. {
  195. return (int) ($this->adminInfo['id'] ?? 0);
  196. }
  197. private function isSuperAdmin(): bool
  198. {
  199. return $this->currentAdminId() === 1 || (int) ($this->adminInfo['is_super'] ?? 0) === 1;
  200. }
  201. }