完善个人中心资金功能

This commit is contained in:
yml2213
2026-07-28 10:41:46 +08:00
parent dca45afcc8
commit 2ab80c4df8
8 changed files with 1960 additions and 23 deletions
@@ -7,17 +7,22 @@ import { findLatestOrderByAnyPlatformOrderId, getOrderById } from '../../reposit
import {
acceptWorkOrderAndSettle,
addWorkerWalletCredit,
countWorkerAcceptedOrders,
countWorkerActiveOrders,
createWorkOrder,
createWorkOrderEvent,
createWorkerFinanceRequest,
createWorkerUser,
getWorkerFinanceRequestSummary,
incrementWorkerSessionVersion,
getWorkOrderById,
getWorkOrderByOrderItemId,
getWorkerUserById,
getWorkerUserByUsername,
listAllWorkCategories,
listWorkerFinanceRequests,
listPendingMaterialWorkOrdersByPlatformOrderId,
listWorkerWalletLedgers,
grabWorkOrder,
listWorkOrders,
listWorkProductRules,
@@ -25,14 +30,17 @@ import {
listWorkerUsers,
resolveProblemWorkOrder,
updateWorkOrder,
updateWorkerPassword,
updateWorkerUser,
upsertWorkCategory,
upsertWorkProductRule,
upsertWorkerLevel,
type WorkCategoryRow,
type WorkerFinanceRequestRow,
type WorkOrderRow,
type WorkProductRuleRow,
type WorkerLevelRow,
type WorkerWalletLedgerRow,
type WorkerUserRow,
type GrabWorkOrderFailureReason,
type ProblemWorkOrderResolutionAction,
@@ -278,9 +286,226 @@ export function requireActiveWorkerSession(session: WorkerSession | null | undef
export async function getWorkerProfile(session: WorkerSession) {
const worker = await getRequiredWorker(session.workerId)
const [financeSummary, acceptedOrderCount] = await Promise.all([
getWorkerFinanceRequestSummary(session.workerId),
countWorkerAcceptedOrders(session.workerId),
])
return {
worker: mapWorkerUser(worker),
permissions: resolveWorkerPermissions(worker),
summary: {
acceptedOrderCount,
pendingWithdrawAmount: financeSummary.pendingWithdrawAmount,
approvedWithdrawAmount: financeSummary.approvedWithdrawAmount,
pendingRechargeAmount: financeSummary.pendingRechargeAmount,
},
}
}
export async function listWorkerProfileWalletLedgers(
query: JsonObject = {},
session: WorkerSession,
) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const ledgerType = normalizeWalletLedgerType(query.ledgerType)
const { items, total } = await listWorkerWalletLedgers({
page,
pageSize,
workerId: session.workerId,
ledgerType,
})
return {
items: items.map(mapWalletLedger),
pagination: { page, pageSize, total },
}
}
export async function listWorkerProfileFinanceRequests(
query: JsonObject = {},
session: WorkerSession,
) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const status = normalizeFinanceRequestStatus(query.status)
const requestType = normalizeFinanceRequestType(query.requestType)
const { items, total } = await listWorkerFinanceRequests({
page,
pageSize,
workerId: session.workerId,
status,
requestType,
})
return {
items: items.map(mapFinanceRequest),
pagination: { page, pageSize, total },
}
}
export async function createWorkerRechargeRequest(
payload: JsonObject = {},
session: WorkerSession,
) {
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
if (amount <= 0) {
throw createHttpError('充值金额必须大于 0', {
statusCode: 400,
errorCode: 'worker_recharge_amount_invalid',
})
}
const note = String(payload.note || payload.remark || '').trim()
const created = await createWorkerFinanceRequest({
workerId: worker.id,
requestType: 'recharge',
amount,
accountChannel: 'manual',
accountName: worker.display_name || worker.username,
accountNo: worker.phone || '',
note: note || '个人中心充值申请',
payloadJson: JSON.stringify({
source: 'worker_profile_recharge',
username: worker.username,
phone: worker.phone || '',
}),
now: nowIso(),
})
if (!created) {
throw createHttpError('充值申请创建失败', {
statusCode: 500,
errorCode: 'worker_recharge_request_create_failed',
})
}
return {
request: mapFinanceRequest(created),
}
}
export async function createWorkerWithdrawRequest(
payload: JsonObject = {},
session: WorkerSession,
) {
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
if (amount <= 0) {
throw createHttpError('提现金额必须大于 0', {
statusCode: 400,
errorCode: 'worker_withdraw_amount_invalid',
})
}
const accountChannel = normalizeWithdrawChannel(
payload.accountChannel || payload.channel,
)
const accountName = String(payload.accountName || payload.realName || '').trim()
const accountNo = String(payload.accountNo || payload.account || '').trim()
if (!accountName) {
throw createHttpError('请填写收款人姓名', {
statusCode: 400,
errorCode: 'worker_withdraw_account_name_required',
})
}
if (!accountNo) {
throw createHttpError('请填写收款账号', {
statusCode: 400,
errorCode: 'worker_withdraw_account_no_required',
})
}
const financeSummary = await getWorkerFinanceRequestSummary(worker.id)
const availableAmount = Number(worker.available_amount || 0)
const availableForWithdraw = Math.max(
0,
availableAmount - financeSummary.pendingWithdrawAmount,
)
if (amount > availableForWithdraw) {
throw createHttpError('可提现余额不足,请先减少提现金额或等待已提交申请处理', {
statusCode: 409,
errorCode: 'worker_withdraw_amount_insufficient',
})
}
const note = String(payload.note || payload.remark || '').trim()
const created = await createWorkerFinanceRequest({
workerId: worker.id,
requestType: 'withdraw',
amount,
accountChannel,
accountName,
accountNo,
note: note || '个人中心提现申请',
payloadJson: JSON.stringify({
source: 'worker_profile_withdraw',
username: worker.username,
phone: worker.phone || '',
}),
now: nowIso(),
})
if (!created) {
throw createHttpError('提现申请创建失败', {
statusCode: 500,
errorCode: 'worker_withdraw_request_create_failed',
})
}
return {
request: mapFinanceRequest(created),
availableForWithdraw,
}
}
export async function changeWorkerPassword(
payload: JsonObject = {},
session: WorkerSession,
) {
const worker = await getRequiredWorker(session.workerId)
const currentPassword = normalizePassword(
payload.currentPassword || payload.oldPassword,
)
const nextPassword = normalizePassword(payload.newPassword || payload.password)
if (!currentPassword) {
throw createHttpError('请输入当前密码', {
statusCode: 400,
errorCode: 'worker_password_current_required',
})
}
if (!verifyWorkerPassword(currentPassword, worker.password_hash)) {
throw createHttpError('当前密码不正确', {
statusCode: 400,
errorCode: 'worker_password_current_invalid',
})
}
if (!nextPassword) {
throw createHttpError('请输入新密码', {
statusCode: 400,
errorCode: 'worker_password_new_required',
})
}
validateWorkerPassword(nextPassword)
if (currentPassword === nextPassword) {
throw createHttpError('新密码不能与当前密码一致', {
statusCode: 400,
errorCode: 'worker_password_unchanged',
})
}
const updated = await updateWorkerPassword({
workerId: worker.id,
passwordHash: hashWorkerPassword(nextPassword),
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
now: nowIso(),
})
return {
worker: mapWorkerUser(updated || worker),
reloginRequired: true,
}
}
@@ -1291,6 +1516,8 @@ function mapWorkerUser(worker: WorkerUserRow) {
wallet: {
availableAmount: Number(worker.available_amount || 0),
frozenDepositAmount: Number(worker.frozen_deposit_amount || 0),
totalCreditedAmount: Number(worker.total_credited_amount || 0),
totalSettledAmount: Number(worker.total_settled_amount || 0),
},
createdAt: worker.created_at,
updatedAt: worker.updated_at,
@@ -1298,6 +1525,43 @@ function mapWorkerUser(worker: WorkerUserRow) {
}
}
function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined) {
if (!ledger) return null
return {
ledgerId: Number(ledger.id),
ledgerType: ledger.ledger_type,
amount: Number(ledger.amount || 0),
balanceAfter: Number(ledger.balance_after || 0),
frozenAfter: Number(ledger.frozen_after || 0),
relatedWorkOrderId: ledger.related_work_order_id
? Number(ledger.related_work_order_id)
: null,
auditStatus: ledger.audit_status || '',
note: ledger.note || '',
payload: safeParseJson(ledger.payload_json),
createdAt: ledger.created_at,
}
}
function mapFinanceRequest(request: WorkerFinanceRequestRow | null | undefined) {
if (!request) return null
return {
requestId: Number(request.id),
requestType: request.request_type || '',
status: request.status || '',
amount: Number(request.amount || 0),
accountChannel: request.account_channel || '',
accountName: request.account_name || '',
accountNo: request.account_no || '',
note: request.note || '',
reviewedNote: request.reviewed_note || '',
payload: safeParseJson(request.payload_json),
createdAt: request.created_at,
updatedAt: request.updated_at,
reviewedAt: request.reviewed_at,
}
}
function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
return {
workOrderId: Number(workOrder.id),
@@ -1632,6 +1896,61 @@ function normalizeEnabledStatus(value: unknown) {
return String(value || 'active').trim() === 'disabled' ? 'disabled' : 'active'
}
function normalizeWalletLedgerType(value: unknown) {
const ledgerType = String(value || '').trim()
if (!ledgerType) return ''
if (
[
'manual_credit',
'deposit_freeze',
'deposit_release',
'deposit_deduction',
'reward_settlement',
].includes(ledgerType)
) {
return ledgerType
}
throw createHttpError('钱包流水类型不正确', {
statusCode: 400,
errorCode: 'worker_wallet_ledger_type_invalid',
})
}
function normalizeFinanceRequestType(value: unknown) {
const requestType = String(value || '').trim()
if (!requestType) return ''
if (['recharge', 'withdraw'].includes(requestType)) {
return requestType
}
throw createHttpError('资金申请类型不正确', {
statusCode: 400,
errorCode: 'worker_finance_request_type_invalid',
})
}
function normalizeFinanceRequestStatus(value: unknown) {
const status = String(value || '').trim()
if (!status) return ''
if (['pending', 'approved', 'rejected', 'cancelled'].includes(status)) {
return status
}
throw createHttpError('资金申请状态不正确', {
statusCode: 400,
errorCode: 'worker_finance_request_status_invalid',
})
}
function normalizeWithdrawChannel(value: unknown) {
const channel = String(value || '').trim()
if (['alipay', 'wechat', 'bank'].includes(channel)) {
return channel
}
throw createHttpError('提现方式不正确', {
statusCode: 400,
errorCode: 'worker_withdraw_channel_invalid',
})
}
function normalizeMatchType(value: unknown) {
const matchType = String(value || '').trim()
if (matchType === 'exact') return 'exact'