Compare commits

...

5 Commits

Author SHA1 Message Date
win
5c88d91382 merge: resolve conflict from origin/zuncle - keep async onShow with phone bind check 2026-04-01 01:30:52 +08:00
63345f4c24 feat: 发货流程增加地址选择弹窗 2026-04-01 00:55:23 +08:00
d7cd33bcca fix(shipping): 反转运费规则为不满5件收运费,满5件包邮
将 onShip() 中运费判断从 > 5 件收运费改为 < 5 件收运费
2026-03-31 21:13:57 +08:00
win
0c794101e7 Merge remote-tracking branch 'origin/zuncle' 2026-03-26 15:39:52 +08:00
27a05210ee fix(auth): 修复活动页和商品详情页未登录即弹登录框导致审核失败
问题背景:
- 平台审核结论:页面未完整浏览、体验详情时即要求授权登录,属于不合规
- 用户应能先浏览页面内容,仅在执行操作(抽奖/兑换/购买)时才引导登录

根因分析:
1. api/appUser.js 中活动浏览类 API(getActivityDetail 等)使用 authRequest,
   虽然后端接口是公开的,但同页面的 getGamePasses 等需认证接口返回 401
   触发全局登录弹窗
2. getProductDetail 使用 authRequest 调用认证接口,未登录直接 401
3. 全局 401 拦截器不区分浏览请求和操作请求

修改内容:
1. api/appUser.js: 6 个浏览类 API 函数从 authRequest 改为 request
   - getActivityDetail, getActivityIssues, getActivityIssueRewards
   - getIssueDrawLogs, getMatchingCardTypes, getProductDetail
   这些接口在后端均为公开路由,不需要携带 token

2. 活动页面 onLoad 中条件调用认证接口:
   - wuxianshang/index.vue: fetchPasses() 仅在已登录时调用
   - yifanshang/index.vue: fetchPasses() 仅在已登录时调用
   - duiduipeng/index.vue: fetchGamePasses() 仅在已登录时调用
   次数卡(game passes)接口需要认证,未登录时跳过即可,
   不影响页面浏览体验

3. utils/request.js: request() 函数增加 suppressAuthModal 参数
   支持调用方按需静默 401 弹窗,作为安全兜底机制

验证场景:
- 未登录 → 打开无限赏/一番赏/对对碰/商品详情 → 正常显示,无登录弹窗
- 未登录 → 点击抽奖/兑换按钮 → 弹出登录提示(符合平台规范)
- 已登录 → 所有功能正常,次数卡信息正常加载
2026-03-26 14:35:26 +08:00
6 changed files with 290 additions and 58 deletions

View File

@ -117,19 +117,19 @@ export function setDefaultAddress(user_id, address_id) {
}
export function getActivityDetail(activity_id) {
return authRequest({ url: `/api/app/activities/${activity_id}`, method: 'GET' })
return request({ url: `/api/app/activities/${activity_id}`, method: 'GET' })
}
export function getActivityIssues(activity_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues`, method: 'GET' })
return request({ url: `/api/app/activities/${activity_id}/issues`, method: 'GET' })
}
export function getActivityIssueRewards(activity_id, issue_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/rewards`, method: 'GET' })
return request({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/rewards`, method: 'GET' })
}
export function getIssueDrawLogs(activity_id, issue_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/draw_logs`, method: 'GET' })
return request({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/draw_logs`, method: 'GET' })
}
export function drawActivityIssue(activity_id, issue_id) {
@ -141,15 +141,17 @@ export function getIssueChoices(activity_id, issue_id) {
}
export function getProductDetail(product_id) {
return authRequest({ url: `/api/app/products/${product_id}`, method: 'GET' })
return request({ url: `/api/app/products/${product_id}`, method: 'GET' })
}
export function redeemInventory(user_id, ids) {
return authRequest({ url: `/api/app/users/${user_id}/inventory/redeem`, method: 'POST', data: { inventory_ids: ids } })
}
export function requestShipping(user_id, ids) {
return authRequest({ url: `/api/app/users/${user_id}/inventory/request-shipping`, method: 'POST', data: { inventory_ids: ids } })
export function requestShipping(user_id, ids, address_id) {
const data = { inventory_ids: ids }
if (address_id) data.address_id = address_id
return authRequest({ url: `/api/app/users/${user_id}/inventory/request-shipping`, method: 'POST', data })
}
export function createShippingFeeOrder(user_id, ids) {
@ -350,7 +352,7 @@ export function playMatchingGame(game_id) {
* 获取所有启用的卡牌配置
*/
export function getMatchingCardTypes() {
return authRequest({ url: '/api/app/matching/card_types', method: 'GET' })
return request({ url: '/api/app/matching/card_types', method: 'GET' })
}
export function createMatchingPreorder({ issue_id, position, coupon_id = 0, item_card_id = 0, use_game_pass = false }) {

View File

@ -1589,8 +1589,10 @@ onLoad((opts) => {
syncResumeGame(id)
fetchDetail(id)
fetchIssues(id)
//
fetchGamePasses()
//
if (uni.getStorageSync('token')) {
fetchGamePasses()
}
}
fetchCardTypes()
})

View File

@ -476,7 +476,9 @@ onLoad(async (opts) => {
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
fetchPasses()
if (uni.getStorageSync('token')) {
fetchPasses()
}
})
//

View File

@ -774,7 +774,9 @@ onLoad(async (opts) => {
fetchWinRecords(id, currentIssueId.value)
}
//
fetchPasses()
if (uni.getStorageSync('token')) {
fetchPasses()
}
})
onUnload(() => {

View File

@ -64,7 +64,7 @@
<view v-if="!hasMore && aggregatedList.length > 0" class="no-more">没有更多了</view>
<!-- 底部操作栏 -->
<view class="bottom-bar" v-if="hasSelected">
<view class="bottom-bar" v-if="hasSelected && !showAddressPicker">
<view class="selected-info">
<text>已选 {{ totalSelectedCount }} </text>
</view>
@ -244,6 +244,41 @@
</view>
</view>
</view>
<!-- 地址选择弹窗 -->
<view class="address-picker-mask" v-if="showAddressPicker" @tap="showAddressPicker = false" @touchmove.stop></view>
<view class="address-picker-popup" :class="{ 'show': showAddressPicker }">
<view class="share-header">
<text class="share-title">选择收货地址</text>
<text class="share-close" @tap="showAddressPicker = false">×</text>
</view>
<scroll-view scroll-y class="address-scroll">
<view v-if="addressList.length === 0" class="empty-address">
<text>暂无收货地址</text>
</view>
<view
v-for="addr in addressList"
:key="addr.id"
class="address-option"
:class="{ selected: selectedAddressId === addr.id }"
@tap="selectedAddressId = addr.id"
>
<view class="addr-radio" :class="{ checked: selectedAddressId === addr.id }"></view>
<view class="addr-info">
<view class="addr-top">
<text class="addr-name">{{ addr.name || addr.realname }}</text>
<text class="addr-phone">{{ addr.phone || addr.mobile }}</text>
<view v-if="addr.is_default" class="addr-default-tag">默认</view>
</view>
<text class="addr-detail">{{ addr.province }} {{ addr.city }} {{ addr.district }} {{ addr.address || addr.detail }}</text>
</view>
</view>
</scroll-view>
<view class="address-picker-footer">
<text class="add-address-link" @tap="toAddAddress">+ 新增收货地址</text>
<button class="confirm-ship-btn" :disabled="!selectedAddressId" @tap="confirmShipWithAddress">确认发货</button>
</view>
</view>
</view>
</template>
@ -272,6 +307,12 @@ const pageSize = ref(100)
const hasMore = ref(true)
const productMetaCache = new Map()
//
const showAddressPicker = ref(false)
const addressList = ref([])
const selectedAddressId = ref(null)
const pendingShipIds = ref([])
// Synthesis tab state
const recipes = ref([])
const synthLoading = ref(false)
@ -311,7 +352,10 @@ async function fetchProductMeta(productId) {
return meta
}
onShow(() => {
onShow(async () => {
//
if (!checkPhoneBoundSync()) return
// Check for external tab switch request
try {
const targetTab = uni.getStorageSync('cabinet_target_tab')
@ -338,18 +382,36 @@ onShow(() => {
return
}
const uid = uni.getStorageSync("user_id")
//
if (pendingShipIds.value.length > 0) {
try {
const addresses = await listAddresses(uid)
const list = addresses.list || addresses.data || addresses || []
addressList.value = Array.isArray(list) ? list : []
if (addressList.value.length > 0) {
if (!selectedAddressId.value) {
const defaultAddr = addressList.value.find(a => a.is_default)
selectedAddressId.value = defaultAddr ? defaultAddr.id : addressList.value[0].id
}
showAddressPicker.value = true
}
} catch (e) {}
return
}
//
page.value = 1
hasMore.value = true
aggregatedList.value = []
shippedList.value = []
const uid = uni.getStorageSync("user_id")
if (currentTab.value === 1) {
loadShipments(uid)
} else if (currentTab.value === 2) {
loadRecipes(uid)
} else {
loadInventory(uid) // onReachBottom
loadInventory(uid)
}
})
@ -772,7 +834,6 @@ async function onShip() {
const selectedItems = aggregatedList.value.filter(item => item.selected)
if (selectedItems.length === 0) return
// inventory id
let allIds = []
selectedItems.forEach(item => {
if (item.original_ids && item.original_ids.length >= item.selectedCount) {
@ -786,42 +847,49 @@ async function onShip() {
return
}
// 1.
//
try {
const addresses = await listAddresses(user_id)
const addressList = addresses.list || addresses.data || addresses || []
const list = addresses.list || addresses.data || addresses || []
addressList.value = Array.isArray(list) ? list : []
if (!addressList || addressList.length === 0) {
//
if (addressList.value.length === 0) {
uni.showModal({
title: '提示',
content: '申请发货需要设置默认地址,是否前往新建地址?',
content: '申请发货需要设置收货地址,是否前往新建地址?',
confirmText: '前往',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages-user/address/edit' })
}
if (res.confirm) uni.navigateTo({ url: '/pages-user/address/edit' })
}
})
return
}
pendingShipIds.value = allIds
const defaultAddr = addressList.value.find(a => a.is_default)
selectedAddressId.value = defaultAddr ? defaultAddr.id : addressList.value[0].id
showAddressPicker.value = true
} catch (e) {
console.error('获取地址列表失败:', e)
uni.showToast({ title: '获取地址失败', icon: 'none' })
return
}
}
async function confirmShipWithAddress() {
const user_id = uni.getStorageSync('user_id')
const allIds = pendingShipIds.value
const addressId = selectedAddressId.value
showAddressPicker.value = false
// 2.
const FREIGHT_THRESHOLD = 5
const FREIGHT_FEE = 10
if (allIds.length > FREIGHT_THRESHOLD) {
// 5 10
if (allIds.length < FREIGHT_THRESHOLD) {
const confirmed = await new Promise((resolve) => {
uni.showModal({
title: '需支付运费',
content: `${allIds.length} 件商品,超过 ${FREIGHT_THRESHOLD} 件需支付 ¥${FREIGHT_FEE}.00 运费,确认继续?`,
content: `${allIds.length} 件商品,不满 ${FREIGHT_THRESHOLD} 件需支付 ¥${FREIGHT_FEE}.00 运费,确认继续?`,
confirmText: '去支付',
cancelText: '取消',
success: (res) => resolve(res.confirm)
@ -843,11 +911,11 @@ async function onShip() {
}
uni.hideLoading()
//
uni.showLoading({ title: '提交中...' })
try {
await requestShipping(user_id, allIds)
await requestShipping(user_id, allIds, addressId)
uni.showToast({ title: '申请成功', icon: 'success' })
pendingShipIds.value = []
aggregatedList.value = []
page.value = 1
hasMore.value = true
@ -860,30 +928,26 @@ async function onShip() {
return
}
// 5
uni.showModal({
title: '确认发货',
content: `${allIds.length} 件物品,确认申请发货?`,
confirmText: '确认发货',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '提交中...' })
try {
await requestShipping(user_id, allIds)
uni.showToast({ title: '申请成功', icon: 'success' })
//
aggregatedList.value = []
page.value = 1
hasMore.value = true
loadInventory(user_id)
} catch (e) {
uni.showToast({ title: e.message || '申请失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
}
})
// 5
uni.showLoading({ title: '提交中...' })
try {
await requestShipping(user_id, allIds, addressId)
uni.showToast({ title: '申请成功', icon: 'success' })
pendingShipIds.value = []
aggregatedList.value = []
page.value = 1
hasMore.value = true
loadInventory(user_id)
} catch (e) {
uni.showToast({ title: e.message || '申请失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
function toAddAddress() {
showAddressPicker.value = false
uni.navigateTo({ url: '/pages-user/address/edit' })
}
// 48
@ -1920,4 +1984,164 @@ function onCopyShareLink() {
0% { left: -100%; }
20%, 100% { left: 200%; }
}
/* ── 地址选择弹窗 ── */
.address-picker-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 1009;
backdrop-filter: blur(4rpx);
}
.address-picker-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 40rpx 40rpx 0 0;
z-index: 1010;
padding: 40rpx 40rpx 0;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
max-height: 70vh;
display: flex;
flex-direction: column;
&.show {
transform: translateY(0);
}
}
.address-scroll {
flex: 1;
max-height: 50vh;
margin: 20rpx 0;
}
.address-option {
display: flex;
align-items: flex-start;
padding: 24rpx;
border-radius: $radius-md;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
background: $bg-page;
transition: all 0.2s;
&.selected {
border-color: $brand-primary;
background: rgba($brand-primary, 0.05);
}
}
.addr-radio {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid $text-tertiary;
margin-right: 20rpx;
margin-top: 6rpx;
flex-shrink: 0;
transition: all 0.2s;
&.checked {
border-color: $brand-primary;
background: $brand-primary;
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16rpx;
height: 8rpx;
border-left: 3rpx solid #fff;
border-bottom: 3rpx solid #fff;
transform: translate(-50%, -65%) rotate(-45deg);
}
}
}
.addr-info {
flex: 1;
min-width: 0;
}
.addr-top {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.addr-name {
font-size: 28rpx;
font-weight: 700;
color: $text-main;
}
.addr-phone {
font-size: 26rpx;
color: $text-sub;
}
.addr-default-tag {
font-size: 18rpx;
color: #fff;
background: $brand-primary;
padding: 2rpx 10rpx;
border-radius: 6rpx;
font-weight: 600;
}
.addr-detail {
font-size: 24rpx;
color: $text-sub;
line-height: 1.5;
}
.empty-address {
text-align: center;
padding: 60rpx 0;
color: $text-tertiary;
font-size: 28rpx;
}
.address-picker-footer {
padding: 20rpx 0 calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
}
.add-address-link {
font-size: 26rpx;
color: $brand-primary;
font-weight: 500;
}
.confirm-ship-btn {
background: $gradient-brand;
color: #fff;
border: none;
border-radius: $radius-round;
height: 80rpx;
padding: 0 60rpx;
font-size: 28rpx;
font-weight: 600;
box-shadow: $shadow-warm;
&[disabled] {
opacity: 0.5;
}
&::after { border: none; }
}
</style>

View File

@ -23,7 +23,7 @@ function handleAuthExpired() {
// 不再脱敏,直接打印原始数据
export function request({ url, method = 'GET', data = {}, header = {} }) {
export function request({ url, method = 'GET', data = {}, header = {}, suppressAuthModal = false }) {
return new Promise((resolve, reject) => {
const finalHeader = { ...buildDefaultHeaders(), ...header }
uni.request({
@ -39,7 +39,7 @@ export function request({ url, method = 'GET', data = {}, header = {} }) {
resolve(body && body.data !== undefined ? body.data : body)
} else {
if (code === 401) {
const suppress = finalHeader && (finalHeader['X-Suppress-Auth-Modal'] || finalHeader['x-suppress-auth-modal'])
const suppress = suppressAuthModal || finalHeader && (finalHeader['X-Suppress-Auth-Modal'] || finalHeader['x-suppress-auth-modal'])
if (!suppress) {
handleAuthExpired()
}