From b350f1c07a57e45d98ee1a45520cd8286e5ae694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=A6=E6=98=9F=E6=98=9F?= <605893810@qq.com> Date: Wed, 9 Sep 2026 11:46:36 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=8A=96=E9=9F=B3=E6=94=AF?= =?UTF-8?q?=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/auth.js | 13 + api/coupon.js | 13 + components/common/guide-popup.vue | 269 +++++ manifest.json | 22 +- pages.json | 12 +- pages/home/home.vue | 320 ++++++ pages/login_package/news/news.vue | 19 +- pages/mine/mine.vue | 2 +- .../mine_address_add/mine_address_add.vue | 307 +++--- .../mine_order_info/mine_order_info.vue | 53 +- pages/mine_package/mine_purse/mine_purse.vue | 142 ++- .../order_submit/order_submit.vue | 25 +- .../douyin_pay_test/douyin_pay_test.vue | 597 ++++++++++++ .../invite_friends/invite_friends.vue | 917 ++++++++++++------ .../hashmall-customer-service/js_sdk/api.js | 2 +- utils/1.md | 233 +++++ utils/enumUtils.js | 6 +- utils/payUtils.js | 249 +++++ 18 files changed, 2670 insertions(+), 531 deletions(-) create mode 100644 components/common/guide-popup.vue create mode 100644 pages/other_package/douyin_pay_test/douyin_pay_test.vue create mode 100644 utils/1.md diff --git a/api/auth.js b/api/auth.js index e5171d6..1998bc7 100644 --- a/api/auth.js +++ b/api/auth.js @@ -175,3 +175,16 @@ export function appleLogin(params) { cha:1 }); } + +/** + * 获取直推邀请用户列表 + * @param {Object} params { page, pageSize } + */ +export function getDirectReferrals(params) { + return request({ + url: "/user/getDirectReferrals", + method: "get", + data: params, + }); +} + diff --git a/api/coupon.js b/api/coupon.js index 3158c8e..ec7abe0 100644 --- a/api/coupon.js +++ b/api/coupon.js @@ -60,3 +60,16 @@ export function getApplicableGoods(data) { data, }); } + +/** + * 查询首页弹窗优惠券通知列表 + * @param {Object} data { page, pageSize } + */ +export function getPopNotifyList(data) { + return request({ + url: "/coupon/getPopNotifyList", + method: "get", + data, + }); +} + diff --git a/components/common/guide-popup.vue b/components/common/guide-popup.vue new file mode 100644 index 0000000..afdac86 --- /dev/null +++ b/components/common/guide-popup.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/manifest.json b/manifest.json index 5cd97e0..7049cdc 100644 --- a/manifest.json +++ b/manifest.json @@ -72,6 +72,16 @@ "com.apple.developer.associated-domains" : [ "applinks:app.tbmall.xin" ] } }, + "urlschemewhitelist" : [ + "snssdk1128", + "douyin", + "douyinopensdk", + "douyinsharesdk", + "alipay", + "alipays", + "weixin", + "wechat" + ], "privacyDescription" : { "NSPhotoLibraryUsageDescription" : "App需要您的同意才能访问相册,以便您选择本地图片上传头像或发布商品评价。例如:当您修改个人头像或发布带图评价时,需要从相册选择图片。", "NSPhotoLibraryAddUsageDescription" : "App需要您的同意才能添加图片到相册,以便您保存商品海报或图片到本地。例如:当您点击保存商品分享海报时,需要写入相册权限。", @@ -141,7 +151,17 @@ "useOriginalMsgbox" : true, "iosStyle" : "common", "androidStyle" : "common" - } + }, + "urlschemewhitelist" : [ + "snssdk1128", + "douyinopensdk", + "douyinsharesdk", + "snssdk141", + "weixin", + "weixinULAPI", + "alipay", + "alipays" + ] }, /*忽略版本提示*/ "compatible" : { diff --git a/pages.json b/pages.json index 6a2eabe..31615ae 100644 --- a/pages.json +++ b/pages.json @@ -659,11 +659,11 @@ { "path": "invite_friends/invite_friends", "style": { - "navigationBarTitleText": "%invite.friends.title%", - "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTitleText": "邀请好友", + "navigationBarBackgroundColor": "#F3ECFF", "navigationBarTextStyle": "black", "enablePullDownRefresh": false, - "navigationStyle": "default" // 默认样式,会显示返回图标 + "navigationStyle": "custom" } }, { @@ -675,6 +675,12 @@ "enablePullDownRefresh": false, "navigationStyle": "default" // 默认样式,会显示返回图标 } + }, + { + "path": "douyin_pay_test/douyin_pay_test", + "style": { + "navigationStyle": "custom" + } } ] }, diff --git a/pages/home/home.vue b/pages/home/home.vue index be4726b..b8cd847 100644 --- a/pages/home/home.vue +++ b/pages/home/home.vue @@ -113,6 +113,8 @@ + 亲星星 + @@ -215,7 +217,66 @@ + + + + + + + + 专属优惠券已到账 + + ✕ + + + + + + + + + + ¥ + {{ item.discountAmount }} + + {{ formatThresholdText(item) }} + + + + + + + + + + + + {{ item.title || '限时秒杀优惠券' }} + + {{ formatScopeBadge(item) }} + {{ item.scopeText || (item.scopeType === 1 ? '所有商品可用' : '部分商品可用') }} + + {{ formatValidityText(item) }} + + + + + + + 立即收下 + + + + + 优惠券详情可在【我的-优惠券】中查看 + + + + + + @@ -238,6 +299,7 @@ import useTabber from "tabber-component"; import { getNewsList } from "@/api/news.js"; import { comparisonVersionHandler } from "@/utils/index.js"; import HomePopup from "@/components/common/home-popup.vue"; +import GuidePopup from "@/components/common/guide-popup.vue"; import productSeckillCard from "@/components/common/product-seckill-card.vue"; import { SHARE_URL } from "@/utils/config.js"; import { getUnreadCount as getKefuUnreadCount } from "@/uni_modules/hashmall-customer-service/js_sdk/api.js"; @@ -246,12 +308,14 @@ import { startKefuRealtime, } from "@/uni_modules/hashmall-customer-service/js_sdk/kefu-realtime.js"; import { getStorageFun, TOKEN_NAME, USER_DATA } from "@/utils/auth.js"; +import { getPopNotifyList } from "@/api/coupon.js"; export default { components: { choiceness, CustomTabBar, HomePopup, + GuidePopup, productSeckillCard, }, data() { @@ -304,6 +368,8 @@ export default { kefuUnreadRefreshTimer: null, isScrolled: false, scrollTop: 0, + showCouponPop: false, + popCouponList: [], }; }, onPageScroll(e) { @@ -337,6 +403,7 @@ export default { this.querySearchList(SEARCH_TYPE.EXPLOSIVE); // 爆单专区 this.onNews(); // 获取公告 this.getCarouselImage(); + this.fetchPopNotifyList(); // 获取专属优惠券弹窗通知 // 秒杀 @@ -394,6 +461,11 @@ export default { // #endif }, methods: { + aaa() { + uni.navigateTo({ + url: '/pages/other_package/douyin_pay_test/douyin_pay_test' + }); + }, getScrollTop(e) { let top = 0; if (typeof e === "number") { @@ -885,6 +957,47 @@ export default { url: "/pages/other_package/productInfo/productInfo?id=" + item.id, }); }, + /** + * 查询专属优惠券弹窗通知 + */ + fetchPopNotifyList() { + if (!getStorageFun(TOKEN_NAME)) { + return; + } + getPopNotifyList({ page: 1, pageSize: 100 }) + .then((res) => { + if (res && res.bizcode === 100) { + const list = res.data?.entitys || res.data || []; + if (Array.isArray(list) && list.length > 0) { + this.popCouponList = list; + this.showCouponPop = true; + } + } + }) + .catch((err) => { + console.error("获取首页优惠券弹窗失败", err); + }); + }, + formatThresholdText(item) { + if (!item || !item.thresholdAmount || Number(item.thresholdAmount) === 0) { + return "无门槛"; + } + return "满减券"; + }, + formatScopeBadge(item) { + if (item && item.scopeType === 1) { + return "通用券"; + } + return "满减券"; + }, + formatValidityText(item) { + if (item && item.startTime && item.endTime) { + const start = dayjs(item.startTime).format("YYYY.MM.DD"); + const end = dayjs(item.endTime).format("YYYY.MM.DD"); + return `${start}-${end}`; + } + return "领取之日起30天有效"; + }, }, computed: { isBlockedDate() { @@ -1657,4 +1770,211 @@ export default { bottom: 60rpx; } } + +/* 专属优惠券弹窗 */ +.coupon_pop_wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.coupon_pop_card { + width: 620rpx; + background: #ffffff; + border-radius: 32rpx; + padding: 36rpx 32rpx 36rpx; + box-sizing: border-box; + position: relative; + + .coupon_pop_header { + display: flex; + align-items: center; + justify-content: center; + position: relative; + margin-bottom: 28rpx; + + .coupon_pop_title { + font-size: 34rpx; + font-weight: bold; + color: #333333; + text-align: center; + } + + .coupon_pop_close { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 44rpx; + height: 44rpx; + // border: 2rpx solid #dcdcdc; + border-radius: 8rpx; + display: flex; + align-items: center; + justify-content: center; + + .close_x { + color: #999999; + font-size: 24rpx; + line-height: 1; + } + } + } + + .coupon_pop_list { + max-height: 520rpx; + margin-bottom: 24rpx; + } + + .coupon_item { + display: flex; + align-items: center; + background: #fff5f5; + border-radius: 20rpx; + height: 160rpx; + margin-bottom: 20rpx; + position: relative; + overflow: hidden; + + &:last-child { + margin-bottom: 0; + } + } + + .coupon_left { + width: 200rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .price_wrap { + display: flex; + align-items: baseline; + color: #ff1e27; + + .currency { + font-size: 28rpx; + font-weight: bold; + margin-right: 2rpx; + } + + .amount { + font-size: 56rpx; + font-weight: bold; + line-height: 1; + } + } + + .threshold_tag { + font-size: 24rpx; + color: #ff1e27; + font-weight: bold; + margin-top: 8rpx; + } + } + + .coupon_divider { + width: 1rpx; + height: 100%; + position: relative; + + .dashed_line { + position: absolute; + top: 16rpx; + bottom: 16rpx; + width: 0; + border-left: 2rpx dashed #ffcdcd; + } + + .notch { + width: 20rpx; + height: 20rpx; + background-color: #ffffff; + border-radius: 50%; + position: absolute; + left: 50%; + transform: translateX(-50%); + + &.notch_top { + top: -10rpx; + } + + &.notch_bottom { + bottom: -10rpx; + } + } + } + + .coupon_right { + flex: 1; + padding-left: 24rpx; + padding-right: 16rpx; + display: flex; + flex-direction: column; + justify-content: center; + + .coupon_name { + font-size: 30rpx; + font-weight: bold; + color: #222222; + margin-bottom: 8rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .coupon_scope_box { + display: flex; + align-items: center; + margin-bottom: 8rpx; + + .scope_badge { + font-size: 20rpx; + color: #ff2437; + border: 1rpx solid #ff8d96; + background: #fff0f0; + border-radius: 6rpx; + padding: 2rpx 10rpx; + margin-right: 12rpx; + flex-shrink: 0; + } + + .scope_text { + font-size: 22rpx; + color: #666666; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .coupon_validity { + font-size: 22rpx; + color: #999999; + } + } + + .coupon_pop_btn { + width: 100%; + height: 88rpx; + line-height: 88rpx; + background: #ff1e27; + border-radius: 44rpx; + color: #ffffff; + font-size: 32rpx; + font-weight: bold; + text-align: center; + margin-top: 10rpx; + } + + .coupon_pop_tip { + font-size: 24rpx; + color: #999999; + text-align: center; + margin-top: 24rpx; + } +} diff --git a/pages/login_package/news/news.vue b/pages/login_package/news/news.vue index 884125f..d3190b2 100644 --- a/pages/login_package/news/news.vue +++ b/pages/login_package/news/news.vue @@ -1,11 +1,12 @@ - + {{ item.title }} @@ -41,14 +42,16 @@ {{ formatDate(item.ctdate) }} - + {{ item.browses }} - + diff --git a/pages/mine/mine.vue b/pages/mine/mine.vue index bbc1444..2942d86 100644 --- a/pages/mine/mine.vue +++ b/pages/mine/mine.vue @@ -381,7 +381,7 @@ export default { // }, { id: 8, - title: '商学院资讯', + title: '资讯', url: 'https://static.tbmall.xin/static/mine/tool_sxy.png', path: '/pages/login_package/news/news' }, diff --git a/pages/mine_package/mine_address_add/mine_address_add.vue b/pages/mine_package/mine_address_add/mine_address_add.vue index 19f9b57..c517880 100644 --- a/pages/mine_package/mine_address_add/mine_address_add.vue +++ b/pages/mine_package/mine_address_add/mine_address_add.vue @@ -1,32 +1,18 @@ + + diff --git a/pages/other_package/invite_friends/invite_friends.vue b/pages/other_package/invite_friends/invite_friends.vue index b45fec7..6a00cab 100644 --- a/pages/other_package/invite_friends/invite_friends.vue +++ b/pages/other_package/invite_friends/invite_friends.vue @@ -1,354 +1,633 @@ - - - - \ No newline at end of file + + /* 仅此处表格列表可垂直滑动,支持 App / 微信小程序 / H5 三端兼容 */ + .table_body_scroll { + flex: 1; + height: 0; + min-height: 0; + box-sizing: border-box; + + /* #ifdef H5 || APP-PLUS */ + ::v-deep uni-scroll-view, + ::v-deep .uni-scroll-view { + height: 100% !important; + } + + /* #endif */ + + .table_body { + .table_row { + display: flex; + border-bottom: 1rpx solid #F0EBF8; + + &:last-child { + border-bottom: none; + } + + .td { + flex: 1; + text-align: center; + padding: 24rpx 8rpx; + font-size: 26rpx; + color: #333333; + display: flex; + align-items: center; + justify-content: center; + word-break: break-all; + + &.td_border { + border-right: 1rpx solid #F0EBF8; + } + } + } + } + } +} + +.load_more_tip { + text-align: center; + padding: 24rpx 0; + font-size: 22rpx; + color: #B0B0B0; +} + +/* 底部固定分享按钮 */ +.bottom_action { + flex-shrink: 0; + background: #FFFFFF; + padding: 20rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04); + box-sizing: border-box; + + .share_btn { + width: 100%; + height: 88rpx; + line-height: 88rpx; + background: linear-gradient(135deg, #9553FF 0%, #7934F6 100%); + border-radius: 44rpx; + color: #FFFFFF; + font-weight: bold; + font-size: 32rpx; + border: none; + outline: none; + text-align: center; + + &:active { + opacity: 0.9; + } + } +} + \ No newline at end of file diff --git a/uni_modules/hashmall-customer-service/js_sdk/api.js b/uni_modules/hashmall-customer-service/js_sdk/api.js index d100267..e2827f7 100644 --- a/uni_modules/hashmall-customer-service/js_sdk/api.js +++ b/uni_modules/hashmall-customer-service/js_sdk/api.js @@ -45,7 +45,7 @@ export function getCurrentMember() { return request({ url: '/user/getUserInfo', method: 'get', isShowLoading: false }).then(result => result.data) } -export function getMessages({ userId, conversationId, page = 1, pageSize = 80 }) { +export function getMessages({ userId, conversationId, page = 1, pageSize = 20 }) { const data = conversationId ? { userId, conversationId, page, pageSize } : { userId, page, pageSize } return kefuRequest({ url: '/kefu/user/messages', diff --git a/utils/1.md b/utils/1.md new file mode 100644 index 0000000..7b5e439 --- /dev/null +++ b/utils/1.md @@ -0,0 +1,233 @@ +# 抖音支付:前端调用商城后端接口文档 + +日期:2026-09-05。适用:商城 App 前端。本文只描述前端调用我们后端的接口,不涉及抖音官方服务端接口、证书配置或插件开发。 + +## 一、公共约定 + +测试环境 Base URL:`https://api.o.tbmall.xin`。生产环境沿用项目正式用户端 API 配置,不要使用后台管理 API 地址。 + +请求头沿用现有登录请求封装: + +```http +Authorization: <项目当前环境的应用授权值> +HSM-AUTH: <当前用户登录令牌> +Content-Type: application/x-www-form-urlencoded +``` + +POST 参数以表单提交,不是 JSON body;GET 使用 URL 查询参数。不要把后台管理 Token 填到 HSM-AUTH。 + +业务响应统一格式: + +```json +{ "bizcode": 100, "errcode": 0, "msg": "成功", "data": {} } +``` + +`bizcode=100` 表示本次接口调用成功,不等于用户已付款。其他业务码按 msg 提示;HTTP 错误、超时或网络失败不能视作付款成功。以下所有 ID、金额、签名均为示例。 + +## 二、调用顺序 + +1. 调用 `/cart/getSupportPay`,判断返回列表是否包含 `type=douyin`。 +2. 根据业务场景选择一个下单/支付接口,提交 `payway=douyin`。 +3. 保存返回的 `data.orderNum`,把 `data.douyin` 中的支付参数交给 App 支付能力拉起抖音。 +4. 用户返回 App 后调用 `/payment/queryResult` 查询结果,只有 `data.state=2` 才提示支付成功。 + +**重点:支付方式筛选传 `types=6`;实际支付传 `payway=douyin`,不是 `payway=6`。** + +## 三、查询支付方式 + +**POST `/cart/getSupportPay`** + +| 参数 | 类型 | 必填 | 说明 | +| ----------- | ------- | ---- | -------------------------------------------- | +| types | string | 否 | 只查抖音传 `6`;多种传 `1,2,6`;全部传 `ALL` | +| isDeduction | boolean | 否 | 默认 false,沿用现有抵扣券结算逻辑 | + +请求表单: + +```text +types=6&isDeduction=false +``` + +已启用抖音支付时的响应示例: + +```json +{ + "bizcode": 100, + "errcode": 0, + "msg": "成功", + "data": [{ "type": "douyin", "name": "抖音支付", "balance": 0 }] +} +``` + +没有启用对应配置时,列表可能为空。显示名称使用 name,提交支付方式使用 type;balance 不是抖音钱包余额。 + +## 四、发起支付(按场景选一个) + +### 4.1 已创建订单,继续支付 + +**POST `/order/payment`** + +| 参数 | 类型 | 必填 | 说明 | +| ------ | ------ | ---- | ----------------------- | +| id | long | 是 | 待支付的商城业务订单 ID | +| payway | string | 是 | 固定 `douyin` | + +```text +id=123456&payway=douyin +``` + +后端接口还接受可选 password/code,但抖音支付不需要提交。成功响应见第五节。 + +### 4.2 商品直接购买并支付 + +**POST `/cart/settlementOrder`** + +| 参数 | 类型 | 必填 | 说明 | +| ------------ | ------- | -------- | ------------------------------------ | +| addrId | long | 是 | 收货地址 ID | +| goodsId | long | 是 | 商品 ID | +| specsId | long | 是 | 规格 ID | +| goodsNum | decimal | 是 | 购买数量 | +| payway | string | 本流程是 | `douyin` | +| payway2 | string | 否 | `vcoin` 表示抵扣券,沿用现有结算选择 | +| couponUserId | long | 否 | 已选用户优惠券 ID | +| remark | string | 否 | 现有分组备注 JSON 字符串 | + +```text +addrId=10001&goodsId=20001&specsId=30001&goodsNum=1&payway=douyin +``` + +remark 格式为 `[{"gkey":"结算返回的分组标识","rmk":"备注内容"}]`,通过现有请求封装表单编码。金额由后端计算,不传前端自算金额。优惠券、抵扣券沿用结算规则,不因选择抖音改变适用范围。 + +### 4.3 购物车结算并支付 + +**POST `/cart/settle`** + +| 参数 | 类型 | 必填 | 说明 | +| ------------ | ------ | ---- | ---------------------------------- | +| addrId | long | 是 | 收货地址 ID | +| cartIds | string | 是 | 购物车 ID 逗号分隔,`ALL` 表示全选 | +| payway | string | 是 | `douyin` | +| couponUserId | long | 否 | 用户优惠券 ID | +| remark | string | 否 | 分组备注 JSON 字符串,同上一接口 | + +```text +addrId=10001&cartIds=40001,40002&payway=douyin +``` + +购物车可能拆分业务订单并合并支付,查询支付结果仍使用本次返回的 orderNum。 + +### 4.4 余额充值 + +**POST `/finance/recharge`** + +| 参数 | 类型 | 必填 | 说明 | +| -------- | ------- | ---- | -------------------- | +| category | string | 是 | 余额充值传 `balance` | +| amount | decimal | 是 | 充值金额,单位元 | +| payway | string | 是 | `douyin` | + +```text +category=balance&amount=1.00&payway=douyin +``` + +注意:上述四个 POST 都可能创建支付单,请勿反复点击或自动重试创建。新下单接口已经返回支付参数时,不要紧接着再调用 `/order/payment`。 + +## 五、下单/支付接口统一返回 + +上述四个接口均返回 PayInfoDto。抖音待支付响应示例(省略其他业务字段): + +```json +{ + "bizcode": 100, + "errcode": 0, + "msg": "成功", + "data": { + "orderId": 123456, + "orderNum": "PAY_EXAMPLE_001", + "amount": 1.0, + "status": 0, + "douyin": { + "appId": "EXAMPLE_APP_ID", + "mchId": "EXAMPLE_MERCHANT_ID", + "prepayId": "EXAMPLE_PREPAY_ID", + "callbackScheme": "trustbridgeapp", + "packageValue": "Sign=DYPay", + "nonceStr": "0123456789abcdef0123456789abcdef", + "timeStamp": "1788585600", + "sign": "EXAMPLE_BASE64_SIGNATURE" + } + } +} +``` + +| 字段 | 类型 | 前端用途 | +| --------------------- | ------- | ---------------------------------------------- | +| orderId | long | 后端返回的订单 ID | +| orderNum | string | 保存下来,支付结果查询必须用它 | +| amount | decimal | 实际支付金额(元) | +| status | integer | 此 DTO 中 0=待支付、1=已支付;不同于查单 state | +| douyin | object | App 拉起抖音所需参数 | +| douyin.appId | string | 抖音应用 ID | +| douyin.mchId | string | 抖音商户 ID | +| douyin.prepayId | string | 预支付 ID | +| douyin.callbackScheme | string | App 返回 Scheme | +| douyin.packageValue | string | `Sign=DYPay` | +| douyin.nonceStr | string | 随机字符串 | +| douyin.timeStamp | string | 秒级时间戳,保留字符串类型 | +| douyin.sign | string | 后端生成的签名,原样使用 | + +后端已处理签名,前端无需请求商户私钥或另调签名接口。参数缺失时停止拉起,提示支付参数异常并反馈后端,不要自行伪造字段。 + +若 App 支付桥接要求 SDK 小写字段,映射如下;这不是另一个 HTTP 请求: + +```js +const d = response.data.douyin; +const payInfo = { + appid: d.appId, + partnerid: d.mchId, + prepayid: d.prepayId, + package: d.packageValue, + noncestr: d.nonceStr, + timestamp: d.timeStamp, + sign: d.sign, +}; +``` + +## 六、查询支付结果 + +**GET `/payment/queryResult`** + +| 参数 | 类型 | 必填 | 说明 | +| -------- | ------ | ---- | ------------------------------------------------------- | +| orderNum | string | 是 | 发起支付接口返回的 data.orderNum,不是 orderId/prepayId | + +```http +GET https://api.o.tbmall.xin/payment/queryResult?orderNum=PAY_EXAMPLE_001 +``` + +```json +{ + "bizcode": 100, + "errcode": 0, + "msg": "成功", + "data": { "state": 2 } +} +``` + +| state | 含义 | 前端处理 | +| ----- | ---------- | -------------------------------- | +| 0 | 订单不存在 | 核对支付编号及环境,不能提示成功 | +| 1 | 支付处理中 | 稍后再次查询 | +| 2 | 支付成功 | 展示成功,刷新订单或充值余额 | +| 3 | 支付失败 | 提示失败,刷新订单状态 | + +当前 App 采用最多 5 次、间隔 1.5 秒的查询;这属于前端策略,不是后端强制限制。暂未确认、断网或超时应显示“支付结果确认中”,保存编号以便回前台再次查询,避免直接重复付款。 + +用户取消或支付 SDK 提示成功后,都需要以服务端查询结果为准。不要调用 `/payment/feedback` 上报成功来替代查单;前端也不需要调用后端的抖音通知/退款回调接口。 + +## 七、对接范围与发布注意 + +- 本文按当前工作区后端代码整理;SDK 签名新增字段(packageValue、nonceStr、timeStamp、sign)尚未确认已部署到测试/生产服务器。 +- 后台配置已完成不代表预下单、签名、支付回调均验收通过,实际支付链路仍需联调。 +- 本次抖音支付是 App 原生支付;H5 仅可检查适用的后端业务请求,不能凭网页调用拉起此原生能力。 diff --git a/utils/enumUtils.js b/utils/enumUtils.js index 2c473c0..95735e1 100644 --- a/utils/enumUtils.js +++ b/utils/enumUtils.js @@ -454,7 +454,7 @@ export const FINANCE_WITHDRAWAL_STATUS = [ ]; /** - * 页面:1-APP首页,2-APP分类页,3-商学院资讯,4-爆单区,5-好物区,6-积分专区,7-数字积分页 8-新人专享、9-超级补贴、10-限时秒杀、11-领券中心 + * 页面:1-APP首页,2-APP分类页,3-资讯,4-爆单区,5-好物区,6-积分专区,7-数字积分页 8-新人专享、9-超级补贴、10-限时秒杀、11-领券中心 * @type {[{value: string, key: string}]} */ export const APP_PAGE_TYPE = { @@ -486,8 +486,8 @@ export const NEWS_TYPE = [ key: 3, value: "新闻" }, { - key: 5, - value: "商学院" + // key: 5, + // value: "商学院" }, // { // key: 4, diff --git a/utils/payUtils.js b/utils/payUtils.js index 4b728ac..8788657 100644 --- a/utils/payUtils.js +++ b/utils/payUtils.js @@ -109,6 +109,255 @@ export function zfbPayFun(alipay, orderId, orderNum) { // }); } +/** + * 抖音支付(兼容 iOS、Android App 端) + * @param {Object|String} dyPay 抖音支付参数 (支持 orderStr, schema, url, payUrl, orderInfo 等) + * @param {Object|String|Number} orderId 支付的订单ID + * @param {Object|String} orderNum 支付的订单编号 + */ +export function douyinPayFun(dyPay, orderId, orderNum) { + console.log("抖音支付---入参:", dyPay, orderId, orderNum); + + return new Promise((resolve, reject) => { + if (!dyPay || (typeof dyPay === "object" && Object.keys(dyPay).length === 0)) { + uni.showToast({ + title: "支付参数错误", + icon: "none", + }); + jumpWayError(); + return reject(new Error("支付参数为空")); + } + + const d = typeof dyPay === "object" ? dyPay : {}; + const sub = (typeof d.orderInfo === "object" && d.orderInfo) ? d.orderInfo : {}; + const type = typeof dyPay === "object" ? (dyPay.type || "app") : "app"; + + // 1. 提取或生成 Scheme / URL (注意:排除 sign,避免将签名误认为 URL) + let orderStr = ""; + if (typeof dyPay === "string") { + orderStr = dyPay; + } else if (typeof dyPay === "object") { + orderStr = + dyPay.orderStr || + dyPay.schema || + dyPay.scheme || + dyPay.url || + dyPay.payUrl || + dyPay.link || + sub.orderStr || + sub.schema || + sub.scheme || + sub.url || + ""; + + // 如果未配置直接链接,则尝试通过 prepayId / pay_token 自动生成抖音收银台 Scheme 备用 + const token = d.prepayId || d.prepayid || sub.prepayid || sub.prepayId; + if (!orderStr && token) { + orderStr = `snssdk1128://pay?pay_token=${encodeURIComponent(token)}`; + } + } + + console.log("抖音支付---提取到的 Scheme / orderStr:", orderStr); + + // 2. 深度合并/映射原生支付所需的 orderInfo(融合根节点与 orderInfo 子节点,同时提供驼峰与小写命名) + const appId = d.appId || d.appid || sub.appId || sub.appid || ""; + const mchId = d.mchId || d.partnerid || d.mchid || sub.mchId || sub.partnerid || sub.mchid || ""; + const prepayId = d.prepayId || d.prepayid || sub.prepayId || sub.prepayid || ""; + const callbackScheme = d.callbackScheme || sub.callbackScheme || "trustbridgeapp"; + const packageValue = d.packageValue || d.package || sub.packageValue || sub.package || "Sign=DYPay"; + const nonceStr = d.nonceStr || d.noncestr || sub.nonceStr || sub.noncestr || ""; + const timeStamp = String(d.timeStamp || d.timestamp || sub.timeStamp || sub.timestamp || ""); + const sign = d.sign || sub.sign || ""; + + const orderInfo = { + // 驼峰命名(标准 uni.requestPayment / 抖音 SDK) + appId, + mchId, + prepayId, + callbackScheme, + packageValue, + nonceStr, + timeStamp, + sign, + // 小写命名(部分原生桥接 SDK 兼容) + appid: appId, + partnerid: mchId, + mchid: mchId, + prepayid: prepayId, + package: packageValue, + noncestr: nonceStr, + timestamp: timeStamp, + service: 5, + }; + + // #ifdef APP-PLUS + // 情形 A:如果显式指定为 H5 方式 + if ( + type === "h5" && + orderStr && + (orderStr.startsWith("http://") || orderStr.startsWith("https://")) + ) { + uni.navigateTo({ + url: + `/pages/other_package/payment_processing/payment_processing?link=${encodeURIComponent( + orderStr + )}&orderNum=` + orderNum, + }); + resolve({ status: "processing" }); + return; + } + + // 情形 B:如果后端直接传入了协议 Scheme 串(如以 snssdk1128://, douyin:// 开头) + if ( + typeof dyPay === "string" || + (dyPay.orderStr && (dyPay.orderStr.startsWith("snssdk") || dyPay.orderStr.startsWith("douyin"))) + ) { + let openSuccess = false; + + // iOS Native.js 尝试 + if (plus.os.name === "iOS") { + try { + const UIApplication = plus.ios.importClass("UIApplication"); + const NSURL = plus.ios.importClass("NSURL"); + const app = UIApplication.sharedApplication(); + const nsUrl = NSURL.URLWithString(orderStr); + if (app && nsUrl && app.openURL(nsUrl)) { + openSuccess = true; + } + } catch (nativeErr) { + console.warn("iOS Native.js openURL 尝试:", nativeErr); + } + } + + // Android Native.js 尝试 + if (plus.os.name === "Android") { + try { + const Intent = plus.android.importClass("android.content.Intent"); + const Uri = plus.android.importClass("android.net.Uri"); + const main = plus.android.runtimeMainActivity(); + const intent = new Intent(Intent.ACTION_VIEW, Uri.parse(orderStr)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + main.startActivity(intent); + openSuccess = true; + } catch (androidErr) { + console.warn("Android Native.js Intent 尝试:", androidErr); + } + } + + if (!openSuccess) { + plus.runtime.openURL( + orderStr, + () => { + if (orderNum) { + uni.navigateTo({ + url: + `/pages/other_package/payment_processing/payment_processing?orderNum=` + + orderNum, + }); + } + resolve({ status: "invoked" }); + }, + (err) => { + console.error("拉起抖音客户端失败:", err); + const isSimulator = + plus.navigator && plus.navigator.isSimulator + ? plus.navigator.isSimulator() + : false; + let tip = "未检测到抖音客户端或拉起失败,请确认是否已安装抖音客户端"; + if (isSimulator) { + tip = "iOS模拟器无法打开第三方应用Scheme,请在安装了抖音的真机上测试"; + } else if (err && err.code === -3) { + tip = "未检测到抖音应用或当前基座未配置Scheme白名单,请确认真机已安装抖音"; + } + uni.showToast({ + title: tip, + icon: "none", + duration: 3000, + }); + jumpWayError(); + reject(err); + } + ); + } else { + if (orderNum) { + uni.navigateTo({ + url: + `/pages/other_package/payment_processing/payment_processing?orderNum=` + + orderNum, + }); + } + resolve({ status: "invoked" }); + } + return; + } + + // 情形 C:使用 uni.requestPayment 调起原生支付,失败时通过生成 Scheme 降级拉起 + uni.requestPayment({ + provider: "toutiao", + orderInfo: orderInfo, + service: 5, + success: function (res) { + console.log("抖音支付成功:", res); + payFeedbackFun(orderId, orderNum, 1); + jumpWayOk(); + resolve(res); + }, + fail: function (err) { + console.error("uni.requestPayment 抖音支付失败/不兼容,尝试 Scheme 降级拉起:", err); + if (orderStr) { + plus.runtime.openURL( + orderStr, + () => { + if (orderNum) { + uni.navigateTo({ + url: + `/pages/other_package/payment_processing/payment_processing?orderNum=` + + orderNum, + }); + } + resolve({ status: "invoked_fallback" }); + }, + (openErr) => { + console.error("openURL 降级失败:", openErr); + jumpWayError(); + reject(err); + } + ); + } else { + jumpWayError(); + reject(err); + } + }, + }); + return; + // #endif + + // 兜底处理 (H5 及非 App 环境) + if (orderStr) { + uni.navigateTo({ + url: + `/pages/other_package/payment_processing/payment_processing?link=${encodeURIComponent( + orderStr + )}&orderNum=` + orderNum, + }); + resolve({ status: "processing" }); + } else { + uni.showToast({ + title: "抖音支付暂仅支持在 App 端调用", + icon: "none", + }); + jumpWayError(); + reject(new Error("抖音支付暂仅支持在 App 端调用")); + } + }); +} + + +// 导出别名方便不同调用习惯 +export const dyPayFun = douyinPayFun; +export const appDyPayFun = douyinPayFun; +export const appDouyinPayFun = douyinPayFun; + // 跳转到支付成功页面 export function jumpWayOk() { uni.navigateTo({