feat(admin): enrich admin panel fields; add user index constraint and customer service

- admin controller: add updatedAt to interview/resume selects; add orderCount,
  todayOrders, totalRevenue to overview
- admin.vue: enrich all tabs with more fields
  - overview: order cards (count, revenue)
  - users: wxOpenid, email, createdAt, interviewCount, vipExpireAt, role badge
  - interviews: user email, updatedAt, summary preview
  - orders: title, type, channel, paidAt, wxTransactionId, refund info
  - resumes: user email, updatedAt
  - share: sharer phone, shareCode, isActive, visitorId(IP), creditedAt
  - admins: email, createdAt
- user.schema: add unique indexes on phone/wxOpenid/email; pre-save hook
  requiring at least one contact method
- user/about: add WeChat contact button (open-type=contact) for customer service
This commit is contained in:
yuzhiran
2026-06-20 22:38:33 +08:00
parent 8ee27fdd32
commit ef4d22a633
5 changed files with 165 additions and 38 deletions
+13 -6
View File
@@ -48,7 +48,7 @@ export class AdminController {
const [
userCount, interviewCount, todayUsers, todayInterviews,
resumeCount, paidDownloadCount,
planStats,
planStats, orderCount, todayOrders, totalRevenue,
] = await Promise.all([
this.userModel.countDocuments().exec(),
this.interviewModel.countDocuments().exec(),
@@ -59,12 +59,19 @@ export class AdminController {
this.userModel.aggregate([
{ $group: { _id: '$plan', count: { $sum: 1 } } },
]).exec(),
this.orderModel.countDocuments().exec(),
this.orderModel.countDocuments({ status: 'success', createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } }).exec(),
this.orderModel.aggregate([
{ $match: { status: 'success' } },
{ $group: { _id: null, total: { $sum: '$amount' } } },
]).exec(),
])
const planBreakdown: Record<string, number> = {}
planStats.forEach(p => { planBreakdown[p._id || 'free'] = p.count })
return {
userCount, interviewCount, todayUsers, todayInterviews,
resumeCount, paidDownloadCount,
resumeCount, paidDownloadCount, orderCount, todayOrders,
totalRevenue: totalRevenue[0]?.total || 0,
planBreakdown,
}
}
@@ -95,8 +102,8 @@ export class AdminController {
this.interviewModel.find()
.sort({ createdAt: -1 })
.skip(skip).limit(+limit)
.populate('userId', 'phone nickname')
.select('position status totalScore questionCount fillerScore fillerDensity summary createdAt')
.populate('userId', 'phone nickname email wxOpenid')
.select('position status totalScore questionCount fillerScore fillerDensity summary createdAt updatedAt')
.lean().exec(),
this.interviewModel.countDocuments().exec(),
])
@@ -217,8 +224,8 @@ export class AdminController {
this.resumeModel.find()
.sort({ createdAt: -1 })
.skip(skip).limit(+limit)
.populate('userId', 'phone nickname')
.select('title targetPosition version paidDownload createdAt')
.populate('userId', 'phone nickname email')
.select('title targetPosition version paidDownload createdAt updatedAt contentHash')
.lean().exec(),
this.resumeModel.countDocuments().exec(),
])
+11 -4
View File
@@ -5,10 +5,10 @@ export type UserDocument = User & Document
@Schema({ timestamps: true })
export class User {
@Prop({ sparse: true })
@Prop({ unique: true, sparse: true })
phone?: string
@Prop({ sparse: true })
@Prop({ unique: true, sparse: true })
wxOpenid?: string
@Prop({ default: '' })
@@ -60,11 +60,18 @@ export class User {
@Prop({ default: false })
isSystemAdmin: boolean
@Prop({ sparse: true })
@Prop({ unique: true, sparse: true })
email?: string
@Prop({ default: '', select: false })
password?: string
}
export const UserSchema = SchemaFactory.createForClass(User)
export const UserSchema = SchemaFactory.createForClass(User)
UserSchema.pre('save', function (next) {
if (!this.phone && !this.wxOpenid && !this.email) {
return next(new Error('用户必须至少有一个联系方式(手机号/微信/邮箱)'))
}
next()
})