Common.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use app\common\exception\UploadException;
  5. use app\common\library\Upload;
  6. use app\common\model\Area;
  7. use app\common\model\Version;
  8. use fast\Random;
  9. use OSS\OssClient;
  10. use think\captcha\Captcha;
  11. use think\Config;
  12. use think\Hook;
  13. use think\Log;
  14. /**
  15. * 公共接口
  16. */
  17. class Common extends Api
  18. {
  19. protected $noNeedLogin = ['init', 'captcha'];
  20. protected $noNeedRight = '*';
  21. /**
  22. * 获取 OSS 配置
  23. */
  24. public static function getOssConfig(): array
  25. {
  26. $config = Config::get('oss');
  27. return is_array($config) ? $config : [];
  28. }
  29. /**
  30. * OSS 配置是否可用
  31. */
  32. public static function isOssEnabled(): bool
  33. {
  34. $config = self::getOssConfig();
  35. return !empty($config['accessKeyId'])
  36. && !empty($config['accessKeySecret'])
  37. && !empty($config['endpoint'])
  38. && !empty($config['bucket']);
  39. }
  40. /**
  41. * 归一化 OSS 对象键
  42. */
  43. public static function normalizeOssObjectKey(string $objectKey): string
  44. {
  45. return ltrim(str_replace('\\', '/', trim($objectKey)), '/');
  46. }
  47. /**
  48. * 上传本地文件到 OSS
  49. *
  50. * @param string $localFullPath 本地文件完整路径
  51. * 示例:D:/phpstudy_pro/WWW/mes-ai-server-api/public/uploads/material/2026-03-25/a.png
  52. * @param string $objectKey OSS 对象键(Bucket 内的相对路径,不要带域名)
  53. * 示例:uploads/material/2026-03-25/a.png
  54. * @return bool true=上传成功;false=未配置/本地文件不存在/上传失败
  55. */
  56. public static function uploadLocalFileToOss(string $localFullPath, string $objectKey): bool
  57. {
  58. if (!self::isOssEnabled() || !is_file($localFullPath)) {
  59. return false;
  60. }
  61. $config = self::getOssConfig();
  62. $objectKey = self::normalizeOssObjectKey($objectKey);
  63. if ($objectKey === '') {
  64. return false;
  65. }
  66. try {
  67. $ossClient = new OssClient(
  68. $config['accessKeyId'],
  69. $config['accessKeySecret'],
  70. $config['endpoint']
  71. );
  72. $ossClient->uploadFile($config['bucket'], $objectKey, $localFullPath);
  73. return true;
  74. } catch (\Throwable $e) {
  75. Log::write('[OSS uploadLocalFileToOss] ' . $e->getMessage() . ' | objectKey=' . $objectKey . ' | local=' . $localFullPath, 'error');
  76. return false;
  77. }
  78. }
  79. /**
  80. * 删除 OSS 对象
  81. */
  82. public static function deleteOssObject(string $objectKeyOrUrl): bool
  83. {
  84. if (!self::isOssEnabled()) {
  85. return false;
  86. }
  87. $config = self::getOssConfig();
  88. $objectKey = self::normalizeOssObjectKey($objectKeyOrUrl);
  89. if ($objectKey === '') {
  90. return false;
  91. }
  92. try {
  93. $ossClient = new OssClient(
  94. $config['accessKeyId'],
  95. $config['accessKeySecret'],
  96. $config['endpoint']
  97. );
  98. $ossClient->deleteObject($config['bucket'], $objectKey);
  99. return true;
  100. } catch (\Throwable $e) {
  101. return false;
  102. }
  103. }
  104. /**
  105. * 把相对路径拼接为完整 OSS URL;已是 http(s) 则原样返回
  106. * 路径$taskInfo['image_url'] = '/uploads/Product/img2img-20260317152818-69b902924548d.png'
  107. * Common::ossFullUrl((string)$taskInfo['image_url']);
  108. */
  109. public static function ossFullUrl(string $path): string
  110. {
  111. $path = trim($path);
  112. if ($path === '' || stripos($path, 'http://') === 0 || stripos($path, 'https://') === 0) {
  113. return $path;
  114. }
  115. $config = self::getOssConfig();
  116. $host = trim((string)($config['host'] ?? ''));
  117. if ($host === '') {
  118. return $path;
  119. }
  120. if (stripos($host, 'http://') !== 0 && stripos($host, 'https://') !== 0) {
  121. $host = 'https://' . $host;
  122. }
  123. return rtrim($host, '/') . '/' . ltrim($path, '/');
  124. }
  125. /**
  126. * product_template.chinese_description 入库:多页为 JSON 数组字符串,避免数组被写成 "Array"。
  127. *
  128. * @param mixed $raw 前端数组、合法 JSON 数组字符串、或历史单段纯文本
  129. */
  130. public static function encodeChineseDescriptionForDb($raw): string
  131. {
  132. if (is_array($raw)) {
  133. return json_encode($raw, JSON_UNESCAPED_UNICODE) ?: '[]';
  134. }
  135. if ($raw === null || $raw === '') {
  136. return '';
  137. }
  138. if (!is_string($raw)) {
  139. return '';
  140. }
  141. $trimmed = trim($raw);
  142. if ($trimmed === '') {
  143. return '';
  144. }
  145. $decoded = json_decode($trimmed, true);
  146. if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
  147. return json_encode($decoded, JSON_UNESCAPED_UNICODE) ?: $trimmed;
  148. }
  149. return $raw;
  150. }
  151. /**
  152. * 读出给前端:合法 JSON 数组则转 array,否则保持原字符串。
  153. *
  154. * @return array|string
  155. */
  156. public static function decodeChineseDescriptionForApi($stored)
  157. {
  158. if ($stored === null || $stored === '') {
  159. return [];
  160. }
  161. if (!is_string($stored)) {
  162. return $stored;
  163. }
  164. $decoded = json_decode($stored, true);
  165. if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
  166. return $decoded;
  167. }
  168. return $stored;
  169. }
  170. public function _initialize()
  171. {
  172. if (isset($_SERVER['HTTP_ORIGIN'])) {
  173. header('Access-Control-Expose-Headers: __token__');//跨域让客户端获取到
  174. }
  175. //跨域检测
  176. check_cors_request();
  177. if (!isset($_COOKIE['PHPSESSID'])) {
  178. Config::set('session.id', $this->request->server("HTTP_SID"));
  179. }
  180. parent::_initialize();
  181. }
  182. /**
  183. * 加载初始化
  184. *
  185. * @ApiParams (name="version", type="string", required=true, description="版本号")
  186. * @ApiParams (name="lng", type="string", required=true, description="经度")
  187. * @ApiParams (name="lat", type="string", required=true, description="纬度")
  188. */
  189. public function init()
  190. {
  191. if ($version = $this->request->request('version')) {
  192. $lng = $this->request->request('lng');
  193. $lat = $this->request->request('lat');
  194. //配置信息
  195. $upload = Config::get('upload');
  196. //如果非服务端中转模式需要修改为中转
  197. if ($upload['storage'] != 'local' && isset($upload['uploadmode']) && $upload['uploadmode'] != 'server') {
  198. //临时修改上传模式为服务端中转
  199. set_addon_config($upload['storage'], ["uploadmode" => "server"], false);
  200. $upload = \app\common\model\Config::upload();
  201. // 上传信息配置后
  202. Hook::listen("upload_config_init", $upload);
  203. $upload = Config::set('upload', array_merge(Config::get('upload'), $upload));
  204. }
  205. $upload['cdnurl'] = $upload['cdnurl'] ? $upload['cdnurl'] : cdnurl('', true);
  206. $upload['uploadurl'] = preg_match("/^((?:[a-z]+:)?\/\/)(.*)/i", $upload['uploadurl']) ? $upload['uploadurl'] : url($upload['storage'] == 'local' ? '/api/common/upload' : $upload['uploadurl'], '', false, true);
  207. $content = [
  208. 'citydata' => Area::getCityFromLngLat($lng, $lat),
  209. 'versiondata' => Version::check($version),
  210. 'uploaddata' => $upload,
  211. 'coverdata' => Config::get("cover"),
  212. ];
  213. $this->success('', $content);
  214. } else {
  215. $this->error(__('Invalid parameters'));
  216. }
  217. }
  218. /**
  219. * 上传文件
  220. * @ApiMethod (POST)
  221. * @ApiParams (name="file", type="file", required=true, description="文件流")
  222. */
  223. public function upload()
  224. {
  225. Config::set('default_return_type', 'json');
  226. //必须设定cdnurl为空,否则cdnurl函数计算错误
  227. Config::set('upload.cdnurl', '');
  228. $chunkid = $this->request->post("chunkid");
  229. if ($chunkid) {
  230. if (!Config::get('upload.chunking')) {
  231. $this->error(__('Chunk file disabled'));
  232. }
  233. $action = $this->request->post("action");
  234. $chunkindex = $this->request->post("chunkindex/d");
  235. $chunkcount = $this->request->post("chunkcount/d");
  236. $filename = $this->request->post("filename");
  237. $method = $this->request->method(true);
  238. if ($action == 'merge') {
  239. $attachment = null;
  240. //合并分片文件
  241. try {
  242. $upload = new Upload();
  243. $attachment = $upload->merge($chunkid, $chunkcount, $filename);
  244. } catch (UploadException $e) {
  245. $this->error($e->getMessage());
  246. }
  247. $this->success(__('Uploaded successful'), ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]);
  248. } elseif ($method == 'clean') {
  249. //删除冗余的分片文件
  250. try {
  251. $upload = new Upload();
  252. $upload->clean($chunkid);
  253. } catch (UploadException $e) {
  254. $this->error($e->getMessage());
  255. }
  256. $this->success();
  257. } else {
  258. //上传分片文件
  259. //默认普通上传文件
  260. $file = $this->request->file('file');
  261. try {
  262. $upload = new Upload($file);
  263. $upload->chunk($chunkid, $chunkindex, $chunkcount);
  264. } catch (UploadException $e) {
  265. $this->error($e->getMessage());
  266. }
  267. $this->success();
  268. }
  269. } else {
  270. $attachment = null;
  271. //默认普通上传文件
  272. $file = $this->request->file('file');
  273. try {
  274. $upload = new Upload($file);
  275. $attachment = $upload->upload();
  276. } catch (UploadException $e) {
  277. $this->error($e->getMessage());
  278. } catch (\Exception $e) {
  279. $this->error($e->getMessage());
  280. }
  281. $this->success(__('Uploaded successful'), ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]);
  282. }
  283. }
  284. /**
  285. * 验证码
  286. * @ApiParams (name="id", type="string", required=true, description="要生成验证码的标识")
  287. * @return \think\Response
  288. */
  289. public function captcha($id = "")
  290. {
  291. \think\Config::set([
  292. 'captcha' => array_merge(config('captcha'), [
  293. 'fontSize' => 44,
  294. 'imageH' => 150,
  295. 'imageW' => 350,
  296. ])
  297. ]);
  298. $captcha = new Captcha((array)Config::get('captcha'));
  299. return $captcha->entry($id);
  300. }
  301. }