publish-gateway.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import { get } from '@/core/http/http-client'
  2. import { HttpError } from '@/core/http/http-error'
  3. import { parseApiResponse, snapshotEnvelope } from '@/core/http/response-evaluator'
  4. import { buildRestPath } from '@/core/config/api-paths'
  5. import appSettings from '@/core/config/app-settings'
  6. import { pullHomeCategories } from '@/features/travel/gateway'
  7. const uploadConfigPath = () => buildRestPath('common/uploadData')
  8. const publishPath = () => buildRestPath('find/find/addData')
  9. const videoUploadPath = () => '/common/localuploadvideo'
  10. function resolveUploadUrl(path) {
  11. if (!path) return ''
  12. if (/^https?:\/\//.test(path)) return path
  13. if (path.startsWith('//')) return `https:${path}`
  14. const base = (appSettings.appurl || '').replace(/\/$/, '')
  15. return `${base}${path.startsWith('/') ? path : `/${path}`}`
  16. }
  17. function unwrapUploadConfig(payload) {
  18. const body = payload && typeof payload === 'object' ? payload : {}
  19. if (body.uploadurl) return body
  20. if (body.data && typeof body.data === 'object') return body.data
  21. return body
  22. }
  23. function parseUploadResponse(raw) {
  24. if (!raw) return ''
  25. if (typeof raw === 'string') {
  26. try {
  27. const parsed = JSON.parse(raw)
  28. if (parsed.data && parsed.data.fullurl) return parsed.data.fullurl
  29. if (parsed.data && parsed.data.url) return parsed.data.url
  30. if (parsed.fullurl) return parsed.fullurl
  31. if (parsed.url) return parsed.url
  32. } catch (_) {
  33. return ''
  34. }
  35. }
  36. if (typeof raw === 'object') {
  37. if (raw.data && raw.data.fullurl) return raw.data.fullurl
  38. if (raw.data && raw.data.url) return raw.data.url
  39. if (raw.fullurl) return raw.fullurl
  40. if (raw.url) return raw.url
  41. }
  42. return ''
  43. }
  44. /**
  45. * 获取图片上传配置(wanlshop/common/uploadData)
  46. */
  47. export async function fetchUploadConfig() {
  48. const { data, raw } = await get(uploadConfigPath())
  49. const cfg = unwrapUploadConfig(data)
  50. if (cfg.uploadurl) return cfg
  51. return unwrapUploadConfig(raw?.res?.data ?? raw?.data)
  52. }
  53. /**
  54. * 上传单张图片,返回相对路径 url
  55. */
  56. export function uploadImageFile(filePath, config) {
  57. return new Promise((resolve, reject) => {
  58. const uploadurl = resolveUploadUrl(config.uploadurl)
  59. if (!uploadurl) {
  60. reject(new Error('上传地址无效'))
  61. return
  62. }
  63. const formData = config.storage === 'local' ? {} : (config.multipart || {})
  64. uni.uploadFile({
  65. url: uploadurl,
  66. filePath,
  67. name: 'file',
  68. formData,
  69. success: (res) => {
  70. const url = parseUploadResponse(res.data)
  71. if (url) resolve(url)
  72. else reject(new Error('上传失败'))
  73. },
  74. fail: (err) => reject(err || new Error('上传失败'))
  75. })
  76. })
  77. }
  78. function postFindData(body) {
  79. return new Promise((resolve, reject) => {
  80. let settled = false
  81. const finish = (fn, value) => {
  82. if (settled) return
  83. settled = true
  84. fn(value)
  85. }
  86. uni.request({
  87. url: publishPath(),
  88. method: 'POST',
  89. data: body,
  90. success(res) {
  91. try {
  92. snapshotEnvelope(res)
  93. const parsed = parseApiResponse(res)
  94. if (parsed.kind === 'success' || parsed.kind === 'passthrough') {
  95. finish(resolve, parsed.data)
  96. return
  97. }
  98. finish(reject, HttpError.fromParsed(parsed))
  99. } catch (err) {
  100. finish(reject, err)
  101. }
  102. },
  103. fail(err) {
  104. finish(reject, HttpError.fromNetwork(err))
  105. },
  106. complete() {
  107. setTimeout(() => {
  108. if (!settled) {
  109. finish(reject, new HttpError('BUSINESS', '发布失败'))
  110. }
  111. }, 0)
  112. }
  113. })
  114. })
  115. }
  116. function normalizeRelativeVideoPath(path) {
  117. if (!path) return ''
  118. const raw = String(path).replace(/\\\//g, '/')
  119. if (/^https?:\/\//i.test(raw)) {
  120. const match = raw.match(/^https?:\/\/[^/]+(\/.*)$/i)
  121. return match ? match[1] : raw
  122. }
  123. return raw.startsWith('/') ? raw : `/${raw}`
  124. }
  125. function resolveVideoPreviewUrl(data) {
  126. if (!data || typeof data !== 'object') return ''
  127. const full = data.fullurl || data.full_url || ''
  128. if (full) return String(full).replace(/\\\//g, '/')
  129. const rel = normalizeRelativeVideoPath(data.url || '')
  130. if (!rel) return ''
  131. const base = (appSettings.cdnurl || appSettings.appurl || '')
  132. .replace(/\/api\/?$/i, '')
  133. .replace(/\/$/, '')
  134. return `${base}${rel}`
  135. }
  136. function parseVideoUploadEnvelope(raw) {
  137. if (!raw) return null
  138. let envelope = raw
  139. if (typeof raw === 'string') {
  140. try {
  141. envelope = JSON.parse(raw)
  142. } catch (_) {
  143. return null
  144. }
  145. }
  146. const body = envelope && envelope.data
  147. if (body && body.video_id != null && (envelope.code === 1 || envelope.code === '1')) {
  148. return {
  149. video_id: body.video_id,
  150. url: normalizeRelativeVideoPath(body.url || ''),
  151. previewUrl: resolveVideoPreviewUrl(body)
  152. }
  153. }
  154. return null
  155. }
  156. /**
  157. * 视频标签列表(/appvideolabel/list)
  158. */
  159. export async function fetchVideoLabels() {
  160. try {
  161. return await pullHomeCategories()
  162. } catch (_) {
  163. return []
  164. }
  165. }
  166. /**
  167. * 上传短视频(/common/localuploadvideo)
  168. */
  169. export function uploadVideoFile(filePath, { fileName, onProgress } = {}) {
  170. return new Promise((resolve, reject) => {
  171. if (!filePath) {
  172. reject(new Error('视频文件无效'))
  173. return
  174. }
  175. const uploadTask = uni.uploadFile({
  176. url: videoUploadPath(),
  177. filePath,
  178. name: 'file',
  179. formData: {
  180. name: fileName || `video-${Date.now()}.mp4`
  181. },
  182. success: (res) => {
  183. const parsed = parseVideoUploadEnvelope(res.data)
  184. if (res.statusCode === 200 && parsed) {
  185. resolve(parsed)
  186. return
  187. }
  188. let message = '视频上传失败'
  189. try {
  190. const envelope = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
  191. if (envelope && envelope.msg) message = String(envelope.msg)
  192. } catch (_) {}
  193. reject(new Error(message))
  194. },
  195. fail: (err) => reject(err || new Error('视频上传失败'))
  196. })
  197. if (onProgress && uploadTask && typeof uploadTask.onProgressUpdate === 'function') {
  198. uploadTask.onProgressUpdate(onProgress)
  199. }
  200. })
  201. }
  202. /**
  203. * 发布种草动态(type=want)
  204. * 使用 uni.request + complete,避免拦截器 return false 时 Promise 永不 settle
  205. */
  206. export function publishGrassPost({ content, images, goodsIds = [] }) {
  207. const body = {
  208. type: 'want',
  209. content,
  210. images,
  211. goods_ids: goodsIds,
  212. video_id: null,
  213. label_id: null
  214. }
  215. return postFindData(body)
  216. }
  217. /**
  218. * 发布短视频(type=video)
  219. */
  220. export function publishVideoPost({ content, videoId, videoUrl, labelId, goodsIds = [] }) {
  221. const body = {
  222. type: 'video',
  223. content,
  224. images: [],
  225. goods_ids: goodsIds,
  226. video_id: videoId,
  227. video: videoUrl || null,
  228. label_id: labelId
  229. }
  230. return postFindData(body)
  231. }