har.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. import request from "@/utils/request";
  2. const API_PATH = "/payment/har";
  3. /**
  4. * 分页结果 — 后端 PageResult 全局 SNAKE_CASE 序列化后 JSON key 为 `list`
  5. * (全局 PageResult<T> 声明的是 items,与后端契约不符;保留 items 兼容字段,
  6. * 调用方按 `?.items || ?.list || []` 双保险解包,与 batch.ts 一致)
  7. */
  8. export interface HarPageResult<T> {
  9. items?: T;
  10. list: T;
  11. total: number;
  12. page_no: number;
  13. page_size: number;
  14. }
  15. /** 商户配置(后端 HarMerchantVO)— merchant_code 由运营在上游报备进件后手工录入 */
  16. export interface HarMerchantVO {
  17. id?: string;
  18. /** 归属租户。超管建号时选定,之后不可改(列表侧供超管分辨各行属于谁) */
  19. tenant_id?: number;
  20. /** 选填。为空 ⇒ 租户级映射(同一租户下不再区分企业) */
  21. enterprise_id?: string;
  22. /** 上游分配的商户编号,全局唯一 */
  23. merchant_code: string;
  24. merchant_name?: string;
  25. contact_name?: string;
  26. contact_mobile?: string;
  27. description?: string;
  28. created_time?: string;
  29. updated_time?: string;
  30. }
  31. /**
  32. * 商户配置保存入参(HarMerchantSaveDTO)— id 为空即新增。
  33. *
  34. * ★ 该端点**只有超管**能调(后端 `@perm.hasRole('SUPERUSER')`),所以调用方必然在
  35. * 超管上下文里:`tenant_id` 新增时必填(替哪个租户建号),修改时后端忽略它。
  36. */
  37. export interface HarMerchantSaveParams {
  38. id?: string;
  39. /** 新增必填;修改时忽略(归属不可改) */
  40. tenant_id?: number;
  41. enterprise_id?: string;
  42. merchant_code: string;
  43. merchant_name?: string;
  44. contact_name?: string;
  45. contact_mobile?: string;
  46. description?: string;
  47. }
  48. /**
  49. * 收款链接(后端 HarCheckoutLinkVO)—— 商户页「收款二维码」弹窗的三格。
  50. *
  51. * ★ 只认这三格,与后端 VO 逐字段对应(租户号、主键、token 都不回给页面 —— 见那个 VO 的 javadoc)。
  52. * ★ `merchant_name` 是**链接行上的快照**,不是商户行那一列:商户改过名但还没重新取过
  53. * 链接时两者会不同,而操作员看到的名字必须与顾客打开链接时看到的一样。
  54. * ★ `url` 整条来自后端 `HarCheckoutLinkService#buildUrl`。**不要**用 `merchant_code`
  55. * 或域名自己拼:基址推导只有那一份,拼第二个副本的后果是「复制的链接」与
  56. * 「二维码里的链接」不是同一条。
  57. */
  58. export interface HarCheckoutLinkVO {
  59. /** 汇安融商户编号 —— 操作员据此确认取的是哪一家 */
  60. merchant_code: string;
  61. /** 商户显示名(**快照**)。可能为 null:商户行没填名字时不编一个出来 */
  62. merchant_name?: string | null;
  63. /** 完整收款链接(形如 `https://<域名>/#/checkout/alipay/<token>`) */
  64. url: string;
  65. }
  66. /** 门店(后端 HarShopVO) */
  67. export interface HarShopVO {
  68. id?: string;
  69. enterprise_id: string;
  70. merchant_code?: string;
  71. /** 商户自己的门店编码,幂等键 */
  72. third_shop_id: string;
  73. shop_name?: string;
  74. /**
  75. * GB/T 2260 六位省码(`320000`=江苏省)。
  76. *
  77. * ★ 可空**只对 2026-10-08 之前建的门店**成立:上游没有门店更新接口,那批店补不上,
  78. * 挑店时会被当成「地区不匹配」落选 ⇒ 页面必须把「未填」显示成未填,
  79. * 不能看着像有地区(见 shop/index.vue 的 regionLabel)。
  80. */
  81. province_code?: string | null;
  82. /** GB/T 2260 六位市码(`320100`=南京市),见 province_code */
  83. city_code?: string | null;
  84. /** 上游返回的平台门店编号,创建成功才有值 */
  85. shop_id?: string;
  86. /** PENDING=结果未知待人工核实 / SUCCESS=上游已确认 / FAILED=上游明确拒绝 */
  87. create_status: string;
  88. fail_reason?: string;
  89. retry_count?: number;
  90. description?: string;
  91. created_time?: string;
  92. updated_time?: string;
  93. }
  94. /** 门店创建入参(HarShopCreateDTO)— 上游无门店查询接口,本地落库是唯一的可查来源 */
  95. export interface HarShopCreateParams {
  96. third_shop_id: string;
  97. shop_name: string;
  98. /**
  99. * 省份编码,**必填**(规范 261008 5.5)。取值只能来自 `regionProvinces()`
  100. * —— 前端不许自己存一份省市表:编码是挑店比对用的键,与后端目录必须同源。
  101. */
  102. province_code: string;
  103. /** 市编码,**必填**,且必须属于上面那个省(后端按省查表校验) */
  104. city_code: string;
  105. /** 选填。为空 ⇒ 走**租户级**商户映射(enterprise_id IS NULL 的那一行) */
  106. enterprise_id?: string;
  107. /**
  108. * 用哪一条商户配置。一个租户可以配置**多个**商户(2026-09-24),此时必须指定:
  109. * 不指定而本租户有多条 ⇒ 后端 400「请在请求中指定 merchant_code」,
  110. * 不会替调用方挑一条(挑错了门店挂到别的商户名下,且没有任何报错)。
  111. * 取值必须是本租户的商户编号,跨租户的会被 404 挡掉。
  112. */
  113. merchant_code?: string;
  114. }
  115. /**
  116. * 一条地区记录(后端 HarRegionVO)—— 省与地级共用这个形状:码 + 名字。
  117. *
  118. * ★ `code` 是提交回 `HarShopCreateParams` 的那一格,`name` 只用于显示。
  119. * ★ 这份目录同时是**顾客 IP 挑店**的比对表 ⇒ 页面选出来的码就是路由用的码。
  120. * 不要在前端另存省市表,也不要对码做加工(补零、截断、拼接)。
  121. */
  122. export interface HarRegionVO {
  123. /** GB/T 2260 完整六位:省 `320000`,地级 `320100` */
  124. code: string;
  125. /** 展示名。直辖市的地级是占位名换成的省名(重庆给出 500100/500200 两个) */
  126. name: string;
  127. }
  128. /** 订单(后端 HarOrderVO)— 状态直接采用上游枚举,不做本地转义 */
  129. export interface HarOrderVO {
  130. id?: string;
  131. enterprise_id: string;
  132. /** 我方订单号(雪花ID),即上游的 third_order_no */
  133. order_no: string;
  134. /** 企业商户自己的订单号 */
  135. out_biz_no?: string;
  136. upstream_order_no?: string;
  137. platform_trade_no?: string;
  138. merchant_code?: string;
  139. // 自 2026-10-09 起响应不含门店两键(shop_code / third_shop_code);请求侧仍可传,见 HarOrderCreateParams
  140. price: string;
  141. title?: string;
  142. /** alipay_stage=花呗分期 / alipay_un_stage=支付宝支付(260924 前叫 alipay_payment) */
  143. trade_type?: string;
  144. /** 业务类型。非分期传 har_ali_un_stage(规范 260924 订单创建参数表 biz 行) */
  145. biz?: string;
  146. stage_num?: string;
  147. /** 100=商户承担 / 0=用户承担 */
  148. percent?: string;
  149. pay_timeout?: number;
  150. extra_params?: string;
  151. /** 主扫下单返回的收款链接;被扫(auth_code)下单时为空 */
  152. pay_url?: string;
  153. /** 0=等待支付 / 1=支付成功 / 2=退款成功 / 4=退款中 / 8=交易关闭 / 16=超时关闭 */
  154. trade_status: string;
  155. trade_time?: string;
  156. refund_time?: string;
  157. /** 退款原因:我方发给上游的那一句。上游不回传,非退款单为 null */
  158. refund_reason?: string;
  159. upstream_create_time?: string;
  160. rcv_amount?: string;
  161. fee_amount?: string;
  162. channel_amount?: string;
  163. /** PENDING=未回调 / SUCCESS / FAILED */
  164. notify_status?: string;
  165. created_time?: string;
  166. updated_time?: string;
  167. }
  168. /** 下单入参(HarOrderCreateDTO)— auth_code 有值走被扫收款,为空走主扫下单 */
  169. export interface HarOrderCreateParams {
  170. /** 选填。为空 ⇒ 走**租户级**商户映射(enterprise_id IS NULL 的那一行) */
  171. enterprise_id?: string;
  172. /**
  173. * 用哪一条商户配置 —— 一个租户可以配置**多个**商户(2026-09-24)。
  174. * 不指定而本租户有多条 ⇒ 后端 400,不替调用方挑一条:
  175. * 挑错了钱会进另一个商户账号,且全程没有报错,对账时才发现。
  176. */
  177. merchant_code?: string;
  178. out_biz_no?: string;
  179. price: number | string;
  180. title: string;
  181. trade_type: string;
  182. /**
  183. * 业务类型。选填。规范 260924 订单创建参数表 biz 行:非分期传 `har_ali_un_stage`。
  184. *
  185. * 后端原样透传、不推导 —— 所以下单表单在非分期时自己带上这个值(见 order/index.vue)。
  186. */
  187. biz?: string;
  188. /** 分期时必填,3/6/12/24 */
  189. stage_num?: string;
  190. /** 分期时必填 */
  191. percent?: string;
  192. third_shop_code?: string;
  193. shop_code?: string;
  194. pay_timeout?: number;
  195. extra_params?: string;
  196. auth_code?: string;
  197. }
  198. /**
  199. * 收银台取码入参(后端 HarCashierCreateDTO)。
  200. *
  201. * ★ 金额单位是**元** —— HAR 全通道如此;乐收赢收银台那份是分(`amount`),别串了。
  202. * ★ 报文里**没有企业号**:归属只来自登录身份(与乐收赢收银台同一条口径)。
  203. */
  204. export interface HarCashierQrParams {
  205. /** 金额(元),下限 0.01 */
  206. price: number;
  207. /** 选填。备注即上游 subject(订单标题);留空后端用「扫码收款」 */
  208. remark?: string;
  209. /**
  210. * 一个租户配了多个商户编号时必须指定;不指定而后端发现多条 ⇒ 400
  211. * (钱进哪个商户账号必须说清楚,不允许服务端随便挑一条)。
  212. */
  213. merchant_code?: string;
  214. }
  215. /** 订单列表查询条件 — 起止日期为 yyyy-MM-dd */
  216. export interface HarOrderQueryParams {
  217. enterprise_id?: string;
  218. merchant_code?: string;
  219. order_no?: string;
  220. out_biz_no?: string;
  221. trade_status?: string;
  222. trade_type?: string;
  223. start_date?: string;
  224. end_date?: string;
  225. page_no?: number;
  226. page_size?: number;
  227. }
  228. /** 交易方式。非分期那个值 260924 起叫 alipay_un_stage(此前是 alipay_payment)。 */
  229. export const HAR_TRADE_TYPE_OPTIONS = [
  230. { label: "支付宝支付", value: "alipay_un_stage" },
  231. { label: "花呗分期", value: "alipay_stage" },
  232. ];
  233. /** 非分期的业务类型(上游 biz)。规范 260924 订单创建参数表 biz 行 只给了这一个取值。 */
  234. export const HAR_BIZ_UN_STAGE = "har_ali_un_stage";
  235. /**
  236. * 「1 支付成功」—— 本通道**唯一**允许发起退款的起点状态。
  237. *
  238. * 有名字才敢在模板里比:写死 "1" 的判据散在三处时,改枚举的人只会改到一处。
  239. * 上游语义:退款只能从已支付发起,4 退款中 / 2 退款成功 / 8 交易关闭 / 16 超时关闭 都会被拒。
  240. */
  241. export const HAR_ORDER_STATUS_PAID = "1";
  242. /**
  243. * 上游订单状态枚举,与后端 HarOrderStatus 逐字一致。
  244. *
  245. * 16 超时关闭是**上游返回**的码(我方手上的两份规范 260924 / 261008 都没定义,
  246. * 语义以上游为准:长时间未支付、上游自行关单)。2026-10-10 定性为未收款终态 ——
  247. * 后端侧的机器可读含义见 HarOrderStatus.TIMEOUT_CLOSED 与它的三个集合成员关系。
  248. */
  249. export const HAR_TRADE_STATUS_TEXT: Record<string, string> = {
  250. "0": "等待支付",
  251. "1": "支付成功",
  252. "2": "退款成功",
  253. "4": "退款中",
  254. "8": "交易关闭",
  255. "16": "超时关闭",
  256. };
  257. export type ElTagType = "primary" | "success" | "warning" | "info" | "danger";
  258. export const HAR_TRADE_STATUS_TAG: Record<string, ElTagType> = {
  259. "0": "warning",
  260. "1": "success",
  261. "2": "info",
  262. "4": "warning",
  263. "8": "info",
  264. "16": "info",
  265. };
  266. export const HAR_SHOP_STATUS_TEXT: Record<string, string> = {
  267. PENDING: "待核实",
  268. SUCCESS: "创建成功",
  269. FAILED: "创建失败",
  270. };
  271. export const HAR_SHOP_STATUS_TAG: Record<string, ElTagType> = {
  272. PENDING: "warning",
  273. SUCCESS: "success",
  274. FAILED: "danger",
  275. };
  276. export const HAR_NOTIFY_STATUS_TEXT: Record<string, string> = {
  277. PENDING: "未回调",
  278. SUCCESS: "回调成功",
  279. FAILED: "回调失败",
  280. };
  281. const HarAPI = {
  282. // ---------- 商户配置 ----------
  283. /** 商户配置列表(分页) */
  284. merchantList(params: {
  285. /** 租户筛选 —— 只有超管看得见所有租户的行(后端对超管跳过租户过滤) */
  286. tenant_id?: number;
  287. enterprise_id?: string;
  288. merchant_code?: string;
  289. merchant_name?: string;
  290. page_no?: number;
  291. page_size?: number;
  292. }) {
  293. return request<ApiResponse<HarPageResult<HarMerchantVO[]>>>({
  294. url: `${API_PATH}/merchant`,
  295. method: "get",
  296. params,
  297. });
  298. },
  299. /** 新增或修改商户配置(id 有值为修改) */
  300. merchantSave(data: HarMerchantSaveParams) {
  301. return request<ApiResponse<HarMerchantVO>>({
  302. url: `${API_PATH}/merchant`,
  303. method: "post",
  304. data,
  305. });
  306. },
  307. /** 删除商户配置 */
  308. merchantDelete(id: string) {
  309. return request<ApiResponse<void>>({
  310. url: `${API_PATH}/merchant/${id}`,
  311. method: "delete",
  312. });
  313. },
  314. /**
  315. * 取(必要时创建)该商户的**收款链接** —— 商户页操作列那个「收款二维码」按钮。
  316. *
  317. * ★ 是 **POST** 不是 GET:首次调用会**写一行**(`pay_har_checkout_link`)。这是刻意的 ——
  318. * 链接要印出来/贴出去,每取一次换一个 token 等于让客户手上那张作废,
  319. * 所以同一个商户反复调用返回**同一条**(服务层的幂等,见 `HarCheckoutLinkService#issue`)。
  320. * 前端**不要**自作主张缓存结果或改成 GET。
  321. *
  322. * ★ `id` 是**字符串** —— 雪花 id 超 JS 安全整数(同 merchantDelete 的说明)。
  323. *
  324. * ★ 权限串复用商户 `list`(不新增权限点 ⇒ **不需要新的菜单 SQL**),所以能看见这一页的
  325. * 角色都点得动;租户也只能取到**自己名下**那一行(后端两道数据闸)。
  326. */
  327. checkoutLink(id: string) {
  328. return request<ApiResponse<HarCheckoutLinkVO>>({
  329. url: `${API_PATH}/merchant/${id}/checkout-link`,
  330. method: "post",
  331. });
  332. },
  333. // ---------- 门店 ----------
  334. /** 门店列表(分页) */
  335. shopList(params: {
  336. enterprise_id?: string;
  337. merchant_code?: string;
  338. third_shop_id?: string;
  339. create_status?: string;
  340. page_no?: number;
  341. page_size?: number;
  342. }) {
  343. return request<ApiResponse<HarPageResult<HarShopVO[]>>>({
  344. url: `${API_PATH}/shop`,
  345. method: "get",
  346. params,
  347. });
  348. },
  349. /** 门店详情 */
  350. shopDetail(id: string) {
  351. return request<ApiResponse<HarShopVO>>({
  352. url: `${API_PATH}/shop/${id}`,
  353. method: "get",
  354. });
  355. },
  356. /**
  357. * 创建门店(同步调用上游)。
  358. * 上游没有门店查询接口,超时后无法核实远端是否已创建 —— 此时本地落 PENDING,
  359. * 页面按「待核实」展示并引导人工与上游核对,不可盲目重试(third_shop_id 是幂等键)
  360. */
  361. shopCreate(data: HarShopCreateParams) {
  362. return request<ApiResponse<HarShopVO>>({
  363. url: `${API_PATH}/shop`,
  364. method: "post",
  365. data,
  366. });
  367. },
  368. // ---------- 地区目录(门店表单的省市选择器)----------
  369. /**
  370. * 省级列表(31 个,港澳台不在内)。
  371. *
  372. * ★ 目录在后端只有一份(`HarRegionCatalog`,同一份也用于按顾客 IP 挑店),
  373. * 所以**不要**在前端另存一份省市表:两份表迟早会分叉,而分叉的后果是
  374. * 运营选出来的门店「永远挑不中」,上游又没有门店更新接口,补不上。
  375. *
  376. * ★ 权限串复用门店的 `list`/`create`(`@perm.hasAny(...)`)⇒ 不需要新的菜单 SQL。
  377. */
  378. regionProvinces() {
  379. return request<ApiResponse<HarRegionVO[]>>({
  380. url: `${API_PATH}/region/provinces`,
  381. method: "get",
  382. });
  383. },
  384. /**
  385. * 某省的地级列表。
  386. *
  387. * ★ 参数名是下划线 `province_code`(后端 `@RequestParam(name = "province_code")`)。
  388. * ★ 省码不在目录里 ⇒ 后端 **400**(不是空数组):空表在下拉里与「这个省真的没有市」
  389. * 长得一样,所以失败要按失败处理,别静默吞掉。
  390. */
  391. regionCities(provinceCode: string) {
  392. return request<ApiResponse<HarRegionVO[]>>({
  393. url: `${API_PATH}/region/cities`,
  394. method: "get",
  395. params: { province_code: provinceCode },
  396. });
  397. },
  398. // ---------- 订单 ----------
  399. /** 订单列表(分页);silent = 后台静默刷新 */
  400. orderList(params: HarOrderQueryParams, silent = false) {
  401. return request<ApiResponse<HarPageResult<HarOrderVO[]>>>({
  402. url: `${API_PATH}/order`,
  403. method: "get",
  404. params,
  405. silent,
  406. });
  407. },
  408. /** 订单详情;silent = 后台静默刷新(详情弹窗实时联动用) */
  409. orderDetail(id: string, silent = false) {
  410. return request<ApiResponse<HarOrderVO>>({
  411. url: `${API_PATH}/order/${id}`,
  412. method: "get",
  413. silent,
  414. });
  415. },
  416. /**
  417. * 后台下单(调用上游)。
  418. * 上游异步通知未回 success 时每 5s 重试、最多 5 次(约 25s 窗口),
  419. * 因此支付结果以主动查询为准,通知只是加速收敛
  420. */
  421. orderCreate(data: HarOrderCreateParams) {
  422. return request<ApiResponse<HarOrderVO>>({
  423. url: `${API_PATH}/order`,
  424. method: "post",
  425. data,
  426. });
  427. },
  428. /** 主动查询上游并回写状态 */
  429. orderSync(id: string) {
  430. return request<ApiResponse<HarOrderVO>>({
  431. url: `${API_PATH}/order/${id}/sync`,
  432. method: "post",
  433. });
  434. },
  435. /**
  436. * 退款(全额,调用上游)。
  437. *
  438. * ★ **全额**:本通道上游没有金额字段,所以没有「退多少」这个选择,按钮上不要暗示能部分退。
  439. * ★ 返回的是**受理结果**(trade_status=4 退款中),**不是已退款到账** ——
  440. * 到账与否由通知/轮询把状态收敛成 2 退款成功,调用方必须按返回值如实提示。
  441. * ★ reason 留空由服务端补默认值:上游对空原因**整笔直接拒**,所以这里不能省成一个
  442. * 「服务端有默认值」的假设 —— 默认值兜的是调用方,界面仍应引导填一句真实原因。
  443. */
  444. orderRefund(id: string, reason?: string) {
  445. return request<ApiResponse<HarOrderVO>>({
  446. url: `${API_PATH}/order/${id}/refund`,
  447. method: "post",
  448. data: { reason },
  449. });
  450. },
  451. // ---------- 收银台(扫码收款)----------
  452. /**
  453. * 收银台取码(主扫,同步调上游 precreate)。
  454. *
  455. * ★ 金额单位是**元**(乐收赢那份是分),下限 0.01。
  456. * ★ 「归属」不在报文里,只来自登录身份 —— 调用方传不了、也不该传企业号。
  457. */
  458. cashierQr(data: HarCashierQrParams) {
  459. return request<ApiResponse<HarOrderVO>>({
  460. url: `${API_PATH}/cashier/qrcode`,
  461. method: "post",
  462. data,
  463. });
  464. },
  465. /**
  466. * 收银台查本单;非终态时后端按 3s 节流主动问一次上游。
  467. * silent = 轮询用,失败不弹 toast(一次上游抖动不该盖住二维码)。
  468. */
  469. cashierStatus(orderNo: string, silent = false) {
  470. return request<ApiResponse<HarOrderVO>>({
  471. url: `${API_PATH}/cashier/${orderNo}`,
  472. method: "get",
  473. silent,
  474. });
  475. },
  476. };
  477. export default HarAPI;