import request from "@/utils/request"; const API_PATH = "/payment/har"; /** * 分页结果 — 后端 PageResult 全局 SNAKE_CASE 序列化后 JSON key 为 `list` * (全局 PageResult 声明的是 items,与后端契约不符;保留 items 兼容字段, * 调用方按 `?.items || ?.list || []` 双保险解包,与 batch.ts 一致) */ export interface HarPageResult { items?: T; list: T; total: number; page_no: number; page_size: number; } /** 商户配置(后端 HarMerchantVO)— merchant_code 由运营在上游报备进件后手工录入 */ export interface HarMerchantVO { id?: string; /** 归属租户。超管建号时选定,之后不可改(列表侧供超管分辨各行属于谁) */ tenant_id?: number; /** 选填。为空 ⇒ 租户级映射(同一租户下不再区分企业) */ enterprise_id?: string; /** 上游分配的商户编号,全局唯一 */ merchant_code: string; merchant_name?: string; contact_name?: string; contact_mobile?: string; description?: string; created_time?: string; updated_time?: string; } /** * 商户配置保存入参(HarMerchantSaveDTO)— id 为空即新增。 * * ★ 该端点**只有超管**能调(后端 `@perm.hasRole('SUPERUSER')`),所以调用方必然在 * 超管上下文里:`tenant_id` 新增时必填(替哪个租户建号),修改时后端忽略它。 */ export interface HarMerchantSaveParams { id?: string; /** 新增必填;修改时忽略(归属不可改) */ tenant_id?: number; enterprise_id?: string; merchant_code: string; merchant_name?: string; contact_name?: string; contact_mobile?: string; description?: string; } /** * 收款链接(后端 HarCheckoutLinkVO)—— 商户页「收款二维码」弹窗的三格。 * * ★ 只认这三格,与后端 VO 逐字段对应(租户号、主键、token 都不回给页面 —— 见那个 VO 的 javadoc)。 * ★ `merchant_name` 是**链接行上的快照**,不是商户行那一列:商户改过名但还没重新取过 * 链接时两者会不同,而操作员看到的名字必须与顾客打开链接时看到的一样。 * ★ `url` 整条来自后端 `HarCheckoutLinkService#buildUrl`。**不要**用 `merchant_code` * 或域名自己拼:基址推导只有那一份,拼第二个副本的后果是「复制的链接」与 * 「二维码里的链接」不是同一条。 */ export interface HarCheckoutLinkVO { /** 汇安融商户编号 —— 操作员据此确认取的是哪一家 */ merchant_code: string; /** 商户显示名(**快照**)。可能为 null:商户行没填名字时不编一个出来 */ merchant_name?: string | null; /** 完整收款链接(形如 `https://<域名>/#/checkout/alipay/`) */ url: string; } /** 门店(后端 HarShopVO) */ export interface HarShopVO { id?: string; enterprise_id: string; merchant_code?: string; /** 商户自己的门店编码,幂等键 */ third_shop_id: string; shop_name?: string; /** * GB/T 2260 六位省码(`320000`=江苏省)。 * * ★ 可空**只对 2026-10-08 之前建的门店**成立:上游没有门店更新接口,那批店补不上, * 挑店时会被当成「地区不匹配」落选 ⇒ 页面必须把「未填」显示成未填, * 不能看着像有地区(见 shop/index.vue 的 regionLabel)。 */ province_code?: string | null; /** GB/T 2260 六位市码(`320100`=南京市),见 province_code */ city_code?: string | null; /** 上游返回的平台门店编号,创建成功才有值 */ shop_id?: string; /** PENDING=结果未知待人工核实 / SUCCESS=上游已确认 / FAILED=上游明确拒绝 */ create_status: string; fail_reason?: string; retry_count?: number; description?: string; created_time?: string; updated_time?: string; } /** 门店创建入参(HarShopCreateDTO)— 上游无门店查询接口,本地落库是唯一的可查来源 */ export interface HarShopCreateParams { third_shop_id: string; shop_name: string; /** * 省份编码,**必填**(规范 261008 5.5)。取值只能来自 `regionProvinces()` * —— 前端不许自己存一份省市表:编码是挑店比对用的键,与后端目录必须同源。 */ province_code: string; /** 市编码,**必填**,且必须属于上面那个省(后端按省查表校验) */ city_code: string; /** 选填。为空 ⇒ 走**租户级**商户映射(enterprise_id IS NULL 的那一行) */ enterprise_id?: string; /** * 用哪一条商户配置。一个租户可以配置**多个**商户(2026-09-24),此时必须指定: * 不指定而本租户有多条 ⇒ 后端 400「请在请求中指定 merchant_code」, * 不会替调用方挑一条(挑错了门店挂到别的商户名下,且没有任何报错)。 * 取值必须是本租户的商户编号,跨租户的会被 404 挡掉。 */ merchant_code?: string; } /** * 一条地区记录(后端 HarRegionVO)—— 省与地级共用这个形状:码 + 名字。 * * ★ `code` 是提交回 `HarShopCreateParams` 的那一格,`name` 只用于显示。 * ★ 这份目录同时是**顾客 IP 挑店**的比对表 ⇒ 页面选出来的码就是路由用的码。 * 不要在前端另存省市表,也不要对码做加工(补零、截断、拼接)。 */ export interface HarRegionVO { /** GB/T 2260 完整六位:省 `320000`,地级 `320100` */ code: string; /** 展示名。直辖市的地级是占位名换成的省名(重庆给出 500100/500200 两个) */ name: string; } /** 订单(后端 HarOrderVO)— 状态直接采用上游枚举,不做本地转义 */ export interface HarOrderVO { id?: string; enterprise_id: string; /** 我方订单号(雪花ID),即上游的 third_order_no */ order_no: string; /** 企业商户自己的订单号 */ out_biz_no?: string; upstream_order_no?: string; platform_trade_no?: string; merchant_code?: string; // 自 2026-10-09 起响应不含门店两键(shop_code / third_shop_code);请求侧仍可传,见 HarOrderCreateParams price: string; title?: string; /** alipay_stage=花呗分期 / alipay_un_stage=支付宝支付(260924 前叫 alipay_payment) */ trade_type?: string; /** 业务类型。非分期传 har_ali_un_stage(规范 260924 订单创建参数表 biz 行) */ biz?: string; stage_num?: string; /** 100=商户承担 / 0=用户承担 */ percent?: string; pay_timeout?: number; extra_params?: string; /** 主扫下单返回的收款链接;被扫(auth_code)下单时为空 */ pay_url?: string; /** 0=等待支付 / 1=支付成功 / 2=退款成功 / 4=退款中 / 8=交易关闭 / 16=超时关闭 */ trade_status: string; trade_time?: string; refund_time?: string; /** 退款原因:我方发给上游的那一句。上游不回传,非退款单为 null */ refund_reason?: string; upstream_create_time?: string; rcv_amount?: string; fee_amount?: string; channel_amount?: string; /** PENDING=未回调 / SUCCESS / FAILED */ notify_status?: string; created_time?: string; updated_time?: string; } /** 下单入参(HarOrderCreateDTO)— auth_code 有值走被扫收款,为空走主扫下单 */ export interface HarOrderCreateParams { /** 选填。为空 ⇒ 走**租户级**商户映射(enterprise_id IS NULL 的那一行) */ enterprise_id?: string; /** * 用哪一条商户配置 —— 一个租户可以配置**多个**商户(2026-09-24)。 * 不指定而本租户有多条 ⇒ 后端 400,不替调用方挑一条: * 挑错了钱会进另一个商户账号,且全程没有报错,对账时才发现。 */ merchant_code?: string; out_biz_no?: string; price: number | string; title: string; trade_type: string; /** * 业务类型。选填。规范 260924 订单创建参数表 biz 行:非分期传 `har_ali_un_stage`。 * * 后端原样透传、不推导 —— 所以下单表单在非分期时自己带上这个值(见 order/index.vue)。 */ biz?: string; /** 分期时必填,3/6/12/24 */ stage_num?: string; /** 分期时必填 */ percent?: string; third_shop_code?: string; shop_code?: string; pay_timeout?: number; extra_params?: string; auth_code?: string; } /** * 收银台取码入参(后端 HarCashierCreateDTO)。 * * ★ 金额单位是**元** —— HAR 全通道如此;乐收赢收银台那份是分(`amount`),别串了。 * ★ 报文里**没有企业号**:归属只来自登录身份(与乐收赢收银台同一条口径)。 */ export interface HarCashierQrParams { /** 金额(元),下限 0.01 */ price: number; /** 选填。备注即上游 subject(订单标题);留空后端用「扫码收款」 */ remark?: string; /** * 一个租户配了多个商户编号时必须指定;不指定而后端发现多条 ⇒ 400 * (钱进哪个商户账号必须说清楚,不允许服务端随便挑一条)。 */ merchant_code?: string; } /** 订单列表查询条件 — 起止日期为 yyyy-MM-dd */ export interface HarOrderQueryParams { enterprise_id?: string; merchant_code?: string; order_no?: string; out_biz_no?: string; trade_status?: string; trade_type?: string; start_date?: string; end_date?: string; page_no?: number; page_size?: number; } /** 交易方式。非分期那个值 260924 起叫 alipay_un_stage(此前是 alipay_payment)。 */ export const HAR_TRADE_TYPE_OPTIONS = [ { label: "支付宝支付", value: "alipay_un_stage" }, { label: "花呗分期", value: "alipay_stage" }, ]; /** 非分期的业务类型(上游 biz)。规范 260924 订单创建参数表 biz 行 只给了这一个取值。 */ export const HAR_BIZ_UN_STAGE = "har_ali_un_stage"; /** * 「1 支付成功」—— 本通道**唯一**允许发起退款的起点状态。 * * 有名字才敢在模板里比:写死 "1" 的判据散在三处时,改枚举的人只会改到一处。 * 上游语义:退款只能从已支付发起,4 退款中 / 2 退款成功 / 8 交易关闭 / 16 超时关闭 都会被拒。 */ export const HAR_ORDER_STATUS_PAID = "1"; /** * 上游订单状态枚举,与后端 HarOrderStatus 逐字一致。 * * 16 超时关闭是**上游返回**的码(我方手上的两份规范 260924 / 261008 都没定义, * 语义以上游为准:长时间未支付、上游自行关单)。2026-10-10 定性为未收款终态 —— * 后端侧的机器可读含义见 HarOrderStatus.TIMEOUT_CLOSED 与它的三个集合成员关系。 */ export const HAR_TRADE_STATUS_TEXT: Record = { "0": "等待支付", "1": "支付成功", "2": "退款成功", "4": "退款中", "8": "交易关闭", "16": "超时关闭", }; export type ElTagType = "primary" | "success" | "warning" | "info" | "danger"; export const HAR_TRADE_STATUS_TAG: Record = { "0": "warning", "1": "success", "2": "info", "4": "warning", "8": "info", "16": "info", }; export const HAR_SHOP_STATUS_TEXT: Record = { PENDING: "待核实", SUCCESS: "创建成功", FAILED: "创建失败", }; export const HAR_SHOP_STATUS_TAG: Record = { PENDING: "warning", SUCCESS: "success", FAILED: "danger", }; export const HAR_NOTIFY_STATUS_TEXT: Record = { PENDING: "未回调", SUCCESS: "回调成功", FAILED: "回调失败", }; const HarAPI = { // ---------- 商户配置 ---------- /** 商户配置列表(分页) */ merchantList(params: { /** 租户筛选 —— 只有超管看得见所有租户的行(后端对超管跳过租户过滤) */ tenant_id?: number; enterprise_id?: string; merchant_code?: string; merchant_name?: string; page_no?: number; page_size?: number; }) { return request>>({ url: `${API_PATH}/merchant`, method: "get", params, }); }, /** 新增或修改商户配置(id 有值为修改) */ merchantSave(data: HarMerchantSaveParams) { return request>({ url: `${API_PATH}/merchant`, method: "post", data, }); }, /** 删除商户配置 */ merchantDelete(id: string) { return request>({ url: `${API_PATH}/merchant/${id}`, method: "delete", }); }, /** * 取(必要时创建)该商户的**收款链接** —— 商户页操作列那个「收款二维码」按钮。 * * ★ 是 **POST** 不是 GET:首次调用会**写一行**(`pay_har_checkout_link`)。这是刻意的 —— * 链接要印出来/贴出去,每取一次换一个 token 等于让客户手上那张作废, * 所以同一个商户反复调用返回**同一条**(服务层的幂等,见 `HarCheckoutLinkService#issue`)。 * 前端**不要**自作主张缓存结果或改成 GET。 * * ★ `id` 是**字符串** —— 雪花 id 超 JS 安全整数(同 merchantDelete 的说明)。 * * ★ 权限串复用商户 `list`(不新增权限点 ⇒ **不需要新的菜单 SQL**),所以能看见这一页的 * 角色都点得动;租户也只能取到**自己名下**那一行(后端两道数据闸)。 */ checkoutLink(id: string) { return request>({ url: `${API_PATH}/merchant/${id}/checkout-link`, method: "post", }); }, // ---------- 门店 ---------- /** 门店列表(分页) */ shopList(params: { enterprise_id?: string; merchant_code?: string; third_shop_id?: string; create_status?: string; page_no?: number; page_size?: number; }) { return request>>({ url: `${API_PATH}/shop`, method: "get", params, }); }, /** 门店详情 */ shopDetail(id: string) { return request>({ url: `${API_PATH}/shop/${id}`, method: "get", }); }, /** * 创建门店(同步调用上游)。 * 上游没有门店查询接口,超时后无法核实远端是否已创建 —— 此时本地落 PENDING, * 页面按「待核实」展示并引导人工与上游核对,不可盲目重试(third_shop_id 是幂等键) */ shopCreate(data: HarShopCreateParams) { return request>({ url: `${API_PATH}/shop`, method: "post", data, }); }, // ---------- 地区目录(门店表单的省市选择器)---------- /** * 省级列表(31 个,港澳台不在内)。 * * ★ 目录在后端只有一份(`HarRegionCatalog`,同一份也用于按顾客 IP 挑店), * 所以**不要**在前端另存一份省市表:两份表迟早会分叉,而分叉的后果是 * 运营选出来的门店「永远挑不中」,上游又没有门店更新接口,补不上。 * * ★ 权限串复用门店的 `list`/`create`(`@perm.hasAny(...)`)⇒ 不需要新的菜单 SQL。 */ regionProvinces() { return request>({ url: `${API_PATH}/region/provinces`, method: "get", }); }, /** * 某省的地级列表。 * * ★ 参数名是下划线 `province_code`(后端 `@RequestParam(name = "province_code")`)。 * ★ 省码不在目录里 ⇒ 后端 **400**(不是空数组):空表在下拉里与「这个省真的没有市」 * 长得一样,所以失败要按失败处理,别静默吞掉。 */ regionCities(provinceCode: string) { return request>({ url: `${API_PATH}/region/cities`, method: "get", params: { province_code: provinceCode }, }); }, // ---------- 订单 ---------- /** 订单列表(分页);silent = 后台静默刷新 */ orderList(params: HarOrderQueryParams, silent = false) { return request>>({ url: `${API_PATH}/order`, method: "get", params, silent, }); }, /** 订单详情;silent = 后台静默刷新(详情弹窗实时联动用) */ orderDetail(id: string, silent = false) { return request>({ url: `${API_PATH}/order/${id}`, method: "get", silent, }); }, /** * 后台下单(调用上游)。 * 上游异步通知未回 success 时每 5s 重试、最多 5 次(约 25s 窗口), * 因此支付结果以主动查询为准,通知只是加速收敛 */ orderCreate(data: HarOrderCreateParams) { return request>({ url: `${API_PATH}/order`, method: "post", data, }); }, /** 主动查询上游并回写状态 */ orderSync(id: string) { return request>({ url: `${API_PATH}/order/${id}/sync`, method: "post", }); }, /** * 退款(全额,调用上游)。 * * ★ **全额**:本通道上游没有金额字段,所以没有「退多少」这个选择,按钮上不要暗示能部分退。 * ★ 返回的是**受理结果**(trade_status=4 退款中),**不是已退款到账** —— * 到账与否由通知/轮询把状态收敛成 2 退款成功,调用方必须按返回值如实提示。 * ★ reason 留空由服务端补默认值:上游对空原因**整笔直接拒**,所以这里不能省成一个 * 「服务端有默认值」的假设 —— 默认值兜的是调用方,界面仍应引导填一句真实原因。 */ orderRefund(id: string, reason?: string) { return request>({ url: `${API_PATH}/order/${id}/refund`, method: "post", data: { reason }, }); }, // ---------- 收银台(扫码收款)---------- /** * 收银台取码(主扫,同步调上游 precreate)。 * * ★ 金额单位是**元**(乐收赢那份是分),下限 0.01。 * ★ 「归属」不在报文里,只来自登录身份 —— 调用方传不了、也不该传企业号。 */ cashierQr(data: HarCashierQrParams) { return request>({ url: `${API_PATH}/cashier/qrcode`, method: "post", data, }); }, /** * 收银台查本单;非终态时后端按 3s 节流主动问一次上游。 * silent = 轮询用,失败不弹 toast(一次上游抖动不该盖住二维码)。 */ cashierStatus(orderNo: string, silent = false) { return request>({ url: `${API_PATH}/cashier/${orderNo}`, method: "get", silent, }); }, }; export default HarAPI;