接单平台新增 VIP 等级体系与邀请绑定
- 等级按累计验收通过工单数自动升级(阈值后台可配),VIP1-5 默认配置 10/30/50/100 - 存量打手统一归 VIP1,原 regular 等级停用 - 注册支持邀请码绑定邀请人,每人生成唯一邀请码 - 新增 worker_level_logs 升级流水表 - 个人中心展示邀请码与升级进度,管理端等级表单支持升级阈值配置
This commit is contained in:
@@ -16,6 +16,8 @@ import {
|
||||
getWorkerFinanceRequestById,
|
||||
getWorkerFinanceRequestSummary,
|
||||
getWorkerLevelById,
|
||||
getWorkerLevelByKey,
|
||||
getWorkerUserByInviteCode,
|
||||
incrementWorkerSessionVersion,
|
||||
getWorkOrderById,
|
||||
getWorkOrderByOrderItemId,
|
||||
@@ -73,9 +75,12 @@ export type WorkerSession = {
|
||||
sessionVersion: number
|
||||
}
|
||||
|
||||
const DEFAULT_LEVEL_KEY = 'regular'
|
||||
const DEFAULT_LEVEL_KEY = 'vip1'
|
||||
const DEFAULT_LEVEL_NAME = 'VIP1'
|
||||
const DEFAULT_CATEGORY_KEY = 'default'
|
||||
const DEFAULT_DEPOSIT_THRESHOLD_AMOUNT = 20_000
|
||||
const INVITE_CODE_LENGTH = 8
|
||||
const INVITE_CODE_RETRY_LIMIT = 5
|
||||
const IGNORABLE_WORKER_AUTH_ERROR_CODES = new Set([
|
||||
'worker_auth_required',
|
||||
'worker_auth_invalid',
|
||||
@@ -86,17 +91,21 @@ const IGNORABLE_WORKER_AUTH_ERROR_CODES = new Set([
|
||||
|
||||
export async function ensureWorkerPlatformDefaults() {
|
||||
const now = nowIso()
|
||||
const level = await upsertWorkerLevel({
|
||||
levelKey: DEFAULT_LEVEL_KEY,
|
||||
name: '普通打手',
|
||||
sortOrder: 100,
|
||||
status: 'active',
|
||||
permissionJson: JSON.stringify({
|
||||
depositFreeAmount: 0,
|
||||
maxActiveOrders: 3,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
const existingLevel = await getWorkerLevelByKey(DEFAULT_LEVEL_KEY)
|
||||
const level =
|
||||
existingLevel ||
|
||||
(await upsertWorkerLevel({
|
||||
levelKey: DEFAULT_LEVEL_KEY,
|
||||
name: DEFAULT_LEVEL_NAME,
|
||||
sortOrder: 10,
|
||||
status: 'active',
|
||||
permissionJson: JSON.stringify({
|
||||
depositFreeAmount: 0,
|
||||
maxActiveOrders: 1,
|
||||
upgradeThreshold: 0,
|
||||
}),
|
||||
now,
|
||||
}))
|
||||
const category = await upsertWorkCategory({
|
||||
categoryKey: DEFAULT_CATEGORY_KEY,
|
||||
name: '默认分类',
|
||||
@@ -113,6 +122,7 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
const password = normalizePassword(payload.password)
|
||||
const displayName = String(payload.displayName || payload.nickname || username).trim()
|
||||
const phone = String(payload.phone || '').trim()
|
||||
const inviteCode = String(payload.inviteCode || '').trim().toUpperCase()
|
||||
|
||||
validateWorkerUsername(username)
|
||||
validateWorkerPassword(password)
|
||||
@@ -124,6 +134,20 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
const inviter = inviteCode ? await getWorkerUserByInviteCode(inviteCode) : null
|
||||
if (inviteCode && !inviter) {
|
||||
throw createHttpError('邀请码无效', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_invite_code_invalid',
|
||||
})
|
||||
}
|
||||
if (inviter && inviter.status !== 'active') {
|
||||
throw createHttpError('邀请人账号不可用', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_inviter_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const defaults = await ensureWorkerPlatformDefaults()
|
||||
const now = nowIso()
|
||||
const created = await createWorkerUser({
|
||||
@@ -133,6 +157,8 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
phone,
|
||||
levelId: defaults.level?.id || null,
|
||||
status: 'pending_review',
|
||||
inviterId: inviter ? Number(inviter.id) : null,
|
||||
inviteCode: await generateInviteCode(now),
|
||||
now,
|
||||
})
|
||||
|
||||
@@ -149,6 +175,23 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function generateInviteCode(now: string): Promise<string> {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
for (let attempt = 0; attempt < INVITE_CODE_RETRY_LIMIT; attempt++) {
|
||||
const candidate = Array.from(
|
||||
{ length: INVITE_CODE_LENGTH },
|
||||
() => chars[Math.floor(crypto.randomInt(chars.length))],
|
||||
).join('')
|
||||
if (!(await getWorkerUserByInviteCode(candidate))) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
throw createHttpError('邀请码生成失败,请重试', {
|
||||
statusCode: 500,
|
||||
errorCode: 'worker_invite_code_generation_failed',
|
||||
})
|
||||
}
|
||||
|
||||
export async function loginWorker(username: unknown, password: unknown) {
|
||||
ensureWorkerAuthConfigured()
|
||||
const normalizedUsername = normalizeUsername(username)
|
||||
@@ -298,19 +341,61 @@ export async function getWorkerProfile(session: WorkerSession) {
|
||||
countWorkerAcceptedOrders(session.workerId),
|
||||
Promise.resolve(getWorkerFinanceConfig()),
|
||||
])
|
||||
const permissions = resolveWorkerPermissions(worker)
|
||||
const levelProgress = await resolveLevelProgress(worker, acceptedOrderCount)
|
||||
return {
|
||||
worker: mapWorkerUser(worker),
|
||||
permissions: resolveWorkerPermissions(worker),
|
||||
permissions,
|
||||
summary: {
|
||||
acceptedOrderCount,
|
||||
pendingWithdrawAmount: financeSummary.pendingWithdrawAmount,
|
||||
approvedWithdrawAmount: financeSummary.approvedWithdrawAmount,
|
||||
pendingRechargeAmount: financeSummary.pendingRechargeAmount,
|
||||
},
|
||||
levelProgress,
|
||||
financeConfig,
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveLevelProgress(
|
||||
worker: WorkerUserRow,
|
||||
acceptedOrderCount: number,
|
||||
): Promise<{
|
||||
currentThreshold: number
|
||||
nextThreshold: number | null
|
||||
progressPercent: number
|
||||
} | null> {
|
||||
if (!worker.level_id) return null
|
||||
const levels = await listWorkerLevels()
|
||||
const active = levels.filter((level) => level.status === 'active')
|
||||
const current = active.find((level) => Number(level.id) === Number(worker.level_id))
|
||||
if (!current) return null
|
||||
const currentThreshold = normalizeInteger(safeParseJson(current.permission_json).upgradeThreshold, 0)
|
||||
const next = active
|
||||
.filter((level) => normalizeInteger(safeParseJson(level.permission_json).upgradeThreshold, 0) > currentThreshold)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
normalizeInteger(safeParseJson(a.permission_json).upgradeThreshold, 0) -
|
||||
normalizeInteger(safeParseJson(b.permission_json).upgradeThreshold, 0),
|
||||
)[0]
|
||||
const nextThreshold = next
|
||||
? normalizeInteger(safeParseJson(next.permission_json).upgradeThreshold, 0)
|
||||
: null
|
||||
return {
|
||||
currentThreshold,
|
||||
nextThreshold,
|
||||
progressPercent:
|
||||
nextThreshold && nextThreshold > currentThreshold
|
||||
? Math.min(
|
||||
100,
|
||||
Math.round(
|
||||
((acceptedOrderCount - currentThreshold) / (nextThreshold - currentThreshold)) * 100,
|
||||
),
|
||||
)
|
||||
: 100,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listWorkerProfileWalletLedgers(
|
||||
query: JsonObject = {},
|
||||
session: WorkerSession,
|
||||
@@ -798,13 +883,14 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) {
|
||||
export async function saveAdminWorkerLevel(payload: JsonObject = {}) {
|
||||
const now = nowIso()
|
||||
const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY
|
||||
const name = String(payload.name || '').trim() || '普通打手'
|
||||
const name = String(payload.name || '').trim() || DEFAULT_LEVEL_NAME
|
||||
const permissions = {
|
||||
depositFreeAmount: normalizeAmountFen(
|
||||
payload.depositFreeAmount ?? payload.depositFreeAmountYuan,
|
||||
0,
|
||||
),
|
||||
maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 3),
|
||||
maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(payload.upgradeThreshold, 0),
|
||||
}
|
||||
const level = await upsertWorkerLevel({
|
||||
levelKey,
|
||||
@@ -1564,7 +1650,8 @@ function resolveWorkerPermissions(worker: WorkerUserRow) {
|
||||
const permission = safeParseJson(worker.level_permission_json)
|
||||
return {
|
||||
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 3),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1588,7 +1675,8 @@ function mapWorkerLevel(level: WorkerLevelRow | null | undefined) {
|
||||
sortOrder: Number(level.sort_order || 100),
|
||||
status: level.status,
|
||||
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 3),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||
createdAt: level.created_at,
|
||||
updatedAt: level.updated_at,
|
||||
}
|
||||
@@ -1645,6 +1733,14 @@ function mapWorkerUser(worker: WorkerUserRow) {
|
||||
phone: worker.phone,
|
||||
status: worker.status,
|
||||
reviewNote: worker.review_note || '',
|
||||
inviteCode: worker.invite_code || '',
|
||||
inviter: worker.inviter_id
|
||||
? {
|
||||
workerId: Number(worker.inviter_id),
|
||||
username: worker.inviter_username || '',
|
||||
displayName: worker.inviter_display_name || worker.inviter_username || '',
|
||||
}
|
||||
: null,
|
||||
level: worker.level_id
|
||||
? {
|
||||
levelId: Number(worker.level_id),
|
||||
|
||||
Reference in New Issue
Block a user