feat: 调整余额支持用户搜索选择(昵称/openid前缀,不再需要手打28位完整openid)+ API前缀匹配
This commit is contained in:
@@ -102,9 +102,21 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 调整余额 -->
|
<!-- 调整余额 -->
|
||||||
<el-dialog v-model="adjustDlg" title="调整用户余额" width="400px">
|
<el-dialog v-model="adjustDlg" title="调整用户余额" width="440px">
|
||||||
<el-form label-width="80px">
|
<el-form label-width="80px">
|
||||||
<el-form-item label="OpenID"><el-input v-model="adjustForm.openid" placeholder="用户openid" /></el-form-item>
|
<el-form-item label="搜索用户">
|
||||||
|
<el-input v-model="adjustForm.search" placeholder="输入昵称或openid前8位" clearable
|
||||||
|
@input="searchUsers" style="width:100%" />
|
||||||
|
<div v-if="searchResults.length" class="search-results">
|
||||||
|
<div v-for="u in searchResults" :key="u._id" class="search-item" @click="pickUser(u)">
|
||||||
|
<span>{{ u.nickname || '未命名' }}</span>
|
||||||
|
<small>{{ (u.openid||'').slice(0,14) }}… | 星尘 {{ u.vpayCoins || 0 }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="pickedUser" class="picked-user">
|
||||||
|
✓ {{ pickedUser.nickname || '未命名' }} | 星尘 {{ pickedUser.vpayCoins || 0 }}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="变动值">
|
<el-form-item label="变动值">
|
||||||
<el-input-number v-model="adjustForm.amount" :min="-100000" :max="100000" />
|
<el-input-number v-model="adjustForm.amount" :min="-100000" :max="100000" />
|
||||||
<span style="margin-left:8px;font-size:12px;color:#999">正=赠送 负=扣减</span>
|
<span style="margin-left:8px;font-size:12px;color:#999">正=赠送 负=扣减</span>
|
||||||
@@ -113,7 +125,7 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="adjustDlg = false">取消</el-button>
|
<el-button @click="adjustDlg = false">取消</el-button>
|
||||||
<el-button type="primary" @click="doAdjust">确认</el-button>
|
<el-button type="primary" :disabled="!pickedUser" @click="doAdjust">确认</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +146,28 @@ const refundCoins = computed(() => {
|
|||||||
if (!o) return 0
|
if (!o) return 0
|
||||||
return o.metadata?.coins || Math.round((o.amount || 0) * 100)
|
return o.metadata?.coins || Math.round((o.amount || 0) * 100)
|
||||||
})
|
})
|
||||||
const adjustDlg = ref(false), adjustForm = ref({ openid: '', amount: 100, reason: '' })
|
const adjustDlg = ref(false), adjustForm = ref({ search: '', amount: 100, reason: '' })
|
||||||
|
const searchResults = ref([]), pickedUser = ref(null)
|
||||||
|
let searchTimer = null
|
||||||
|
|
||||||
|
function searchUsers() {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
|
pickedUser.value = null
|
||||||
|
searchResults.value = []
|
||||||
|
const kw = adjustForm.value.search?.trim()
|
||||||
|
if (!kw || kw.length < 2) return
|
||||||
|
searchTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const r = await apiGet(`/api/admin/users?search=${encodeURIComponent(kw)}&limit=5`)
|
||||||
|
searchResults.value = (r.items || []).slice(0, 5)
|
||||||
|
} catch (e) {}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
function pickUser(u) {
|
||||||
|
pickedUser.value = u
|
||||||
|
searchResults.value = []
|
||||||
|
adjustForm.value.search = u.nickname || (u.openid||'').slice(0,12)
|
||||||
|
}
|
||||||
|
|
||||||
function fmt(t) { return t ? new Date(t).toLocaleString('zh-CN', { hour12: false }) : '—' }
|
function fmt(t) { return t ? new Date(t).toLocaleString('zh-CN', { hour12: false }) : '—' }
|
||||||
|
|
||||||
@@ -173,10 +206,17 @@ async function doRefund() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function doAdjust() {
|
async function doAdjust() {
|
||||||
|
if (!pickedUser.value) return ElMessage.warning('请先搜索并选择用户')
|
||||||
try {
|
try {
|
||||||
await apiPost('/api/admin/vpay/adjust', adjustForm.value)
|
await apiPost('/api/admin/vpay/adjust', {
|
||||||
ElMessage.success('调整成功')
|
openid: pickedUser.value.openid,
|
||||||
|
amount: adjustForm.value.amount,
|
||||||
|
reason: adjustForm.value.reason,
|
||||||
|
})
|
||||||
|
ElMessage.success(`调整成功:${pickedUser.value.nickname || '用户'} ${(adjustForm.value.amount > 0 ? '+' : '') + adjustForm.value.amount} 星尘`)
|
||||||
adjustDlg.value = false
|
adjustDlg.value = false
|
||||||
|
pickedUser.value = null
|
||||||
|
adjustForm.value = { search: '', amount: 100, reason: '' }
|
||||||
loadLedger(); loadStats()
|
loadLedger(); loadStats()
|
||||||
} catch (e) { ElMessage.error(e.message || '调整失败') }
|
} catch (e) { ElMessage.error(e.message || '调整失败') }
|
||||||
}
|
}
|
||||||
@@ -192,3 +232,22 @@ onMounted(() => { loadStats(); load(); loadLedger() })
|
|||||||
.scard i { font-size: 11px; color: #a6abbb; font-style: normal; }
|
.scard i { font-size: 11px; color: #a6abbb; font-style: normal; }
|
||||||
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-results {
|
||||||
|
position: absolute; top: 100%; left: 0; right: 0; z-index: 10;
|
||||||
|
background: #fff; border: 1px solid #e4e7ed; border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,.1); max-height: 200px; overflow-y: auto;
|
||||||
|
}
|
||||||
|
.search-item {
|
||||||
|
padding: 8px 12px; cursor: pointer; display: flex; flex-direction: column; gap: 2px;
|
||||||
|
border-bottom: 1px solid #f5f7fa;
|
||||||
|
}
|
||||||
|
.search-item:hover { background: #f5f7fa; }
|
||||||
|
.search-item small { color: #999; font-size: 11px; }
|
||||||
|
.picked-user {
|
||||||
|
margin-top: 6px; padding: 6px 10px; background: #f0f9eb;
|
||||||
|
border: 1px solid #e1f3d8; border-radius: 6px; font-size: 13px; color: #67c23a;
|
||||||
|
}
|
||||||
|
.el-form-item { position: relative; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1339,8 +1339,14 @@ router.post('/vpay/adjust', requirePermission('orders:write'), async (req, res)
|
|||||||
try {
|
try {
|
||||||
const { openid, amount, reason } = req.body
|
const { openid, amount, reason } = req.body
|
||||||
if (!openid || !amount || Math.abs(amount) > 100000) return ApiResponse.error(res, '参数无效', 400)
|
if (!openid || !amount || Math.abs(amount) > 100000) return ApiResponse.error(res, '参数无效', 400)
|
||||||
const user = await User.findOne({ openid })
|
// 支持前缀匹配:输入至少8位openid前缀即可(完整openid有28位,手打太长)
|
||||||
if (!user) return ApiResponse.notFound(res, '用户不存在')
|
let user = await User.findOne({ openid })
|
||||||
|
if (!user && openid.length >= 8) {
|
||||||
|
const matches = await User.find({ openid: { $regex: '^' + openid } }).limit(5)
|
||||||
|
if (matches.length === 1) user = matches[0]
|
||||||
|
else if (matches.length > 1) return ApiResponse.error(res, `匹配到 ${matches.length} 个用户,请输入更多字符`, 400)
|
||||||
|
}
|
||||||
|
if (!user) return ApiResponse.notFound(res, '用户不存在(支持输入openid前8位以上)')
|
||||||
|
|
||||||
user.vpayCoins = (user.vpayCoins || 0) + amount
|
user.vpayCoins = (user.vpayCoins || 0) + amount
|
||||||
await user.save()
|
await user.save()
|
||||||
|
|||||||
Reference in New Issue
Block a user