feat: 调整余额支持用户搜索选择(昵称/openid前缀,不再需要手打28位完整openid)+ API前缀匹配

This commit is contained in:
2026-09-14 13:48:32 +08:00
parent 83d907f2bf
commit b5d6071a3e
2 changed files with 73 additions and 8 deletions
+65 -6
View File
@@ -102,9 +102,21 @@
</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-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-input-number v-model="adjustForm.amount" :min="-100000" :max="100000" />
<span style="margin-left:8px;font-size:12px;color:#999">正=赠送 负=扣减</span>
@@ -113,7 +125,7 @@
</el-form>
<template #footer>
<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>
</el-dialog>
</div>
@@ -134,7 +146,28 @@ const refundCoins = computed(() => {
if (!o) return 0
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 }) : '—' }
@@ -173,10 +206,17 @@ async function doRefund() {
}
}
async function doAdjust() {
if (!pickedUser.value) return ElMessage.warning('请先搜索并选择用户')
try {
await apiPost('/api/admin/vpay/adjust', adjustForm.value)
ElMessage.success('调整成功')
await apiPost('/api/admin/vpay/adjust', {
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
pickedUser.value = null
adjustForm.value = { search: '', amount: 100, reason: '' }
loadLedger(); loadStats()
} catch (e) { ElMessage.error(e.message || '调整失败') }
}
@@ -192,3 +232,22 @@ onMounted(() => { loadStats(); load(); loadLedger() })
.scard i { font-size: 11px; color: #a6abbb; font-style: normal; }
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
</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>
+8 -2
View File
@@ -1339,8 +1339,14 @@ router.post('/vpay/adjust', requirePermission('orders:write'), async (req, res)
try {
const { openid, amount, reason } = req.body
if (!openid || !amount || Math.abs(amount) > 100000) return ApiResponse.error(res, '参数无效', 400)
const user = await User.findOne({ openid })
if (!user) return ApiResponse.notFound(res, '用户不存在')
// 支持前缀匹配:输入至少8位openid前缀即可(完整openid有28位,手打太长)
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
await user.save()