完善个人中心资金功能

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
@@ -0,0 +1,34 @@
-- 006_worker_profile_finance_requests.sql —— 接单端个人中心资金申请能力。
--
-- 说明:
-- 1. 充值 / 提现先走申请单,由后台后续审核处理;
-- 2. 不直接改动现有钱包结算逻辑,避免影响抢单、验收、押金释放链路。
CREATE TABLE IF NOT EXISTS worker_finance_requests (
id BIGSERIAL PRIMARY KEY,
worker_id BIGINT NOT NULL REFERENCES worker_users(id) ON DELETE CASCADE,
request_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
amount INTEGER NOT NULL DEFAULT 0,
account_channel TEXT NOT NULL DEFAULT '',
account_name TEXT NOT NULL DEFAULT '',
account_no TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '',
reviewed_note TEXT NOT NULL DEFAULT '',
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
reviewed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_worker_finance_requests_worker_created
ON worker_finance_requests(worker_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_worker_finance_requests_status_type
ON worker_finance_requests(status, request_type, id DESC);
COMMENT ON TABLE worker_finance_requests IS '打手充值 / 提现申请记录';
COMMENT ON COLUMN worker_finance_requests.request_type IS '申请类型:recharge / withdraw';
COMMENT ON COLUMN worker_finance_requests.status IS '申请状态:pending / approved / rejected / cancelled';
COMMENT ON COLUMN worker_finance_requests.amount IS '申请金额,单位分';
COMMENT ON COLUMN worker_finance_requests.payload_json IS '扩展字段,便于后续接支付凭证、审核信息';
@@ -31,6 +31,8 @@ export type WorkerUserRow = {
level_permission_json?: string | Record<string, unknown> level_permission_json?: string | Record<string, unknown>
available_amount?: number available_amount?: number
frozen_deposit_amount?: number frozen_deposit_amount?: number
total_credited_amount?: number
total_settled_amount?: number
} }
export type WorkerWalletRow = { export type WorkerWalletRow = {
@@ -43,6 +45,37 @@ export type WorkerWalletRow = {
updated_at: string updated_at: string
} }
export type WorkerWalletLedgerRow = {
id: number
worker_id: number
ledger_type: string
amount: number
balance_after: number
frozen_after: number
related_work_order_id: number | null
audit_status: string
note: string
payload_json: string | Record<string, unknown>
created_at: string
}
export type WorkerFinanceRequestRow = {
id: number
worker_id: number
request_type: string
status: string
amount: number
account_channel: string
account_name: string
account_no: string
note: string
reviewed_note: string
payload_json: string | Record<string, unknown>
created_at: string
updated_at: string
reviewed_at: string | null
}
export type WorkCategoryRow = { export type WorkCategoryRow = {
id: number id: number
category_key: string category_key: string
@@ -133,6 +166,21 @@ type ProductRuleListInput = {
keyword?: string keyword?: string
} }
type WalletLedgerListInput = {
page?: number
pageSize?: number
workerId?: number
ledgerType?: string
}
type FinanceRequestListInput = {
page?: number
pageSize?: number
workerId?: number
status?: string
requestType?: string
}
type CreateWorkerInput = { type CreateWorkerInput = {
username: string username: string
passwordHash: string passwordHash: string
@@ -167,7 +215,9 @@ const WORKER_USER_SELECT = `
wl.name AS level_name, wl.name AS level_name,
wl.permission_json AS level_permission_json, wl.permission_json AS level_permission_json,
ww.available_amount, ww.available_amount,
ww.frozen_deposit_amount ww.frozen_deposit_amount,
ww.total_credited_amount,
ww.total_settled_amount
FROM worker_users wu FROM worker_users wu
LEFT JOIN worker_levels wl ON wl.id = wu.level_id LEFT JOIN worker_levels wl ON wl.id = wu.level_id
LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id
@@ -333,6 +383,30 @@ export async function updateWorkerUser(
return getWorkerUserById(workerId) return getWorkerUserById(workerId)
} }
export async function updateWorkerPassword(input: {
workerId: number
passwordHash: string
sessionVersion: number
now: string
}): Promise<WorkerUserRow | null> {
const result = await query<{ id: number }>(
`
UPDATE worker_users
SET
password_hash = $1,
session_version = $2,
updated_at = $3
WHERE id = $4
RETURNING id
`,
[input.passwordHash, input.sessionVersion, input.now, input.workerId],
)
if (!result.rows[0]) {
return null
}
return getWorkerUserById(input.workerId)
}
export async function incrementWorkerSessionVersion( export async function incrementWorkerSessionVersion(
workerId: number | string, workerId: number | string,
now: string, now: string,
@@ -394,6 +468,38 @@ export async function getWorkerWallet(workerId: number | string): Promise<Worker
return result.rows[0] || null return result.rows[0] || null
} }
export async function listWorkerWalletLedgers({
page = 1,
pageSize = 20,
workerId = 0,
ledgerType = '',
}: WalletLedgerListInput = {}): Promise<{ items: WorkerWalletLedgerRow[]; total: number }> {
const { whereClause, params } = buildWorkerWalletLedgerWhere({
workerId,
ledgerType,
})
const totalResult = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM worker_wallet_ledgers wwl ${whereClause}`,
params,
)
const offset = (page - 1) * pageSize
params.push(pageSize, offset)
const itemsResult = await query<WorkerWalletLedgerRow>(
`
SELECT *
FROM worker_wallet_ledgers wwl
${whereClause}
ORDER BY wwl.created_at DESC, wwl.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
)
return {
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
export async function addWorkerWalletCredit(input: { export async function addWorkerWalletCredit(input: {
workerId: number workerId: number
amount: number amount: number
@@ -438,6 +544,121 @@ export async function addWorkerWalletCredit(input: {
}) })
} }
export async function createWorkerFinanceRequest(input: {
workerId: number
requestType: string
amount: number
accountChannel: string
accountName: string
accountNo: string
note: string
payloadJson: string
now: string
}): Promise<WorkerFinanceRequestRow | null> {
const result = await query<WorkerFinanceRequestRow>(
`
INSERT INTO worker_finance_requests (
worker_id, request_type, status, amount,
account_channel, account_name, account_no,
note, reviewed_note, payload_json, created_at, updated_at
) VALUES ($1, $2, 'pending', $3, $4, $5, $6, $7, '', $8::jsonb, $9, $10)
RETURNING *
`,
[
input.workerId,
input.requestType,
input.amount,
input.accountChannel,
input.accountName,
input.accountNo,
input.note,
input.payloadJson,
input.now,
input.now,
],
)
return result.rows[0] || null
}
export async function listWorkerFinanceRequests({
page = 1,
pageSize = 20,
workerId = 0,
status = '',
requestType = '',
}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> {
const { whereClause, params } = buildWorkerFinanceRequestWhere({
workerId,
status,
requestType,
})
const totalResult = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM worker_finance_requests wfr ${whereClause}`,
params,
)
const offset = (page - 1) * pageSize
params.push(pageSize, offset)
const itemsResult = await query<WorkerFinanceRequestRow>(
`
SELECT *
FROM worker_finance_requests wfr
${whereClause}
ORDER BY wfr.created_at DESC, wfr.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
)
return {
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
export async function getWorkerFinanceRequestSummary(workerId: number | string) {
const result = await query<{
pending_withdraw_amount: number
approved_withdraw_amount: number
pending_recharge_amount: number
}>(
`
SELECT
COALESCE(
SUM(CASE WHEN request_type = 'withdraw' AND status = 'pending' THEN amount ELSE 0 END),
0
)::int AS pending_withdraw_amount,
COALESCE(
SUM(CASE WHEN request_type = 'withdraw' AND status = 'approved' THEN amount ELSE 0 END),
0
)::int AS approved_withdraw_amount,
COALESCE(
SUM(CASE WHEN request_type = 'recharge' AND status = 'pending' THEN amount ELSE 0 END),
0
)::int AS pending_recharge_amount
FROM worker_finance_requests
WHERE worker_id = $1
`,
[Number(workerId)],
)
return {
pendingWithdrawAmount: Number(result.rows[0]?.pending_withdraw_amount || 0),
approvedWithdrawAmount: Number(result.rows[0]?.approved_withdraw_amount || 0),
pendingRechargeAmount: Number(result.rows[0]?.pending_recharge_amount || 0),
}
}
export async function countWorkerAcceptedOrders(workerId: number | string): Promise<number> {
const result = await query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_orders
WHERE assigned_worker_id = $1
AND status = 'accepted'
`,
[Number(workerId)],
)
return Number(result.rows[0]?.total || 0)
}
export async function getWorkCategoryByKey(categoryKey: string): Promise<WorkCategoryRow | null> { export async function getWorkCategoryByKey(categoryKey: string): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>( const result = await query<WorkCategoryRow>(
'SELECT * FROM work_categories WHERE category_key = $1 LIMIT 1', 'SELECT * FROM work_categories WHERE category_key = $1 LIMIT 1',
@@ -1255,6 +1476,51 @@ function buildWorkOrderWhere({ status = '', keyword = '', workerId = 0 }: ListIn
} }
} }
function buildWorkerWalletLedgerWhere({
workerId = 0,
ledgerType = '',
}: WalletLedgerListInput) {
const filters: string[] = []
const params: unknown[] = []
if (workerId) {
params.push(workerId)
filters.push(`wwl.worker_id = $${params.length}`)
}
if (ledgerType) {
params.push(ledgerType)
filters.push(`wwl.ledger_type = $${params.length}`)
}
return {
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params,
}
}
function buildWorkerFinanceRequestWhere({
workerId = 0,
status = '',
requestType = '',
}: FinanceRequestListInput) {
const filters: string[] = []
const params: unknown[] = []
if (workerId) {
params.push(workerId)
filters.push(`wfr.worker_id = $${params.length}`)
}
if (status) {
params.push(status)
filters.push(`wfr.status = $${params.length}`)
}
if (requestType) {
params.push(requestType)
filters.push(`wfr.request_type = $${params.length}`)
}
return {
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params,
}
}
function buildWorkProductRuleWhere({ enabled = null, keyword = '' }: ProductRuleListInput) { function buildWorkProductRuleWhere({ enabled = null, keyword = '' }: ProductRuleListInput) {
const filters: string[] = [] const filters: string[] = []
const params: unknown[] = [] const params: unknown[] = []
+52
View File
@@ -4,9 +4,14 @@ import { createRateLimitMiddleware, getBodyFieldRateLimitKey } from '../middlewa
import { uploadSingleFile } from './file-upload.js' import { uploadSingleFile } from './file-upload.js'
import { uploadFileAsset } from '../services/file-storage/file-storage-service.js' import { uploadFileAsset } from '../services/file-storage/file-storage-service.js'
import { import {
changeWorkerPassword,
createWorkerRechargeRequest,
createWorkerWithdrawRequest,
getWorkerProfile, getWorkerProfile,
getWorkerSessionSummary, getWorkerSessionSummary,
grabWorkerHallOrder, grabWorkerHallOrder,
listWorkerProfileFinanceRequests,
listWorkerProfileWalletLedgers,
listWorkerHallOrders, listWorkerHallOrders,
listWorkerMyOrders, listWorkerMyOrders,
loginWorker, loginWorker,
@@ -84,6 +89,53 @@ router.get(
}), }),
) )
router.get(
'/profile/wallet-ledgers',
createRouteHandler((req) => listWorkerProfileWalletLedgers(req.query, getRequiredWorkerSession(req)), {
successMessage: 'ok',
errorMessage: '读取钱包流水失败',
scope: '[worker/profile/wallet-ledgers]',
}),
)
router.get(
'/profile/finance-requests',
createRouteHandler((req) => listWorkerProfileFinanceRequests(req.query, getRequiredWorkerSession(req)), {
successMessage: 'ok',
errorMessage: '读取资金申请记录失败',
scope: '[worker/profile/finance-requests]',
}),
)
router.post(
'/profile/recharge-requests',
requireActiveWorker,
createRouteHandler((req) => createWorkerRechargeRequest(req.body || {}, getRequiredWorkerSession(req)), {
successMessage: '充值申请已提交',
errorMessage: '提交充值申请失败',
scope: '[worker/profile/recharge-requests]',
}),
)
router.post(
'/profile/withdraw-requests',
requireActiveWorker,
createRouteHandler((req) => createWorkerWithdrawRequest(req.body || {}, getRequiredWorkerSession(req)), {
successMessage: '提现申请已提交',
errorMessage: '提交提现申请失败',
scope: '[worker/profile/withdraw-requests]',
}),
)
router.post(
'/profile/change-password',
createRouteHandler((req) => changeWorkerPassword(req.body || {}, getRequiredWorkerSession(req)), {
successMessage: '密码已修改',
errorMessage: '修改密码失败',
scope: '[worker/profile/change-password]',
}),
)
router.post( router.post(
'/files/upload', '/files/upload',
requireActiveWorker, requireActiveWorker,
@@ -7,17 +7,22 @@ import { findLatestOrderByAnyPlatformOrderId, getOrderById } from '../../reposit
import { import {
acceptWorkOrderAndSettle, acceptWorkOrderAndSettle,
addWorkerWalletCredit, addWorkerWalletCredit,
countWorkerAcceptedOrders,
countWorkerActiveOrders, countWorkerActiveOrders,
createWorkOrder, createWorkOrder,
createWorkOrderEvent, createWorkOrderEvent,
createWorkerFinanceRequest,
createWorkerUser, createWorkerUser,
getWorkerFinanceRequestSummary,
incrementWorkerSessionVersion, incrementWorkerSessionVersion,
getWorkOrderById, getWorkOrderById,
getWorkOrderByOrderItemId, getWorkOrderByOrderItemId,
getWorkerUserById, getWorkerUserById,
getWorkerUserByUsername, getWorkerUserByUsername,
listAllWorkCategories, listAllWorkCategories,
listWorkerFinanceRequests,
listPendingMaterialWorkOrdersByPlatformOrderId, listPendingMaterialWorkOrdersByPlatformOrderId,
listWorkerWalletLedgers,
grabWorkOrder, grabWorkOrder,
listWorkOrders, listWorkOrders,
listWorkProductRules, listWorkProductRules,
@@ -25,14 +30,17 @@ import {
listWorkerUsers, listWorkerUsers,
resolveProblemWorkOrder, resolveProblemWorkOrder,
updateWorkOrder, updateWorkOrder,
updateWorkerPassword,
updateWorkerUser, updateWorkerUser,
upsertWorkCategory, upsertWorkCategory,
upsertWorkProductRule, upsertWorkProductRule,
upsertWorkerLevel, upsertWorkerLevel,
type WorkCategoryRow, type WorkCategoryRow,
type WorkerFinanceRequestRow,
type WorkOrderRow, type WorkOrderRow,
type WorkProductRuleRow, type WorkProductRuleRow,
type WorkerLevelRow, type WorkerLevelRow,
type WorkerWalletLedgerRow,
type WorkerUserRow, type WorkerUserRow,
type GrabWorkOrderFailureReason, type GrabWorkOrderFailureReason,
type ProblemWorkOrderResolutionAction, type ProblemWorkOrderResolutionAction,
@@ -278,9 +286,226 @@ export function requireActiveWorkerSession(session: WorkerSession | null | undef
export async function getWorkerProfile(session: WorkerSession) { export async function getWorkerProfile(session: WorkerSession) {
const worker = await getRequiredWorker(session.workerId) const worker = await getRequiredWorker(session.workerId)
const [financeSummary, acceptedOrderCount] = await Promise.all([
getWorkerFinanceRequestSummary(session.workerId),
countWorkerAcceptedOrders(session.workerId),
])
return { return {
worker: mapWorkerUser(worker), worker: mapWorkerUser(worker),
permissions: resolveWorkerPermissions(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: { wallet: {
availableAmount: Number(worker.available_amount || 0), availableAmount: Number(worker.available_amount || 0),
frozenDepositAmount: Number(worker.frozen_deposit_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, createdAt: worker.created_at,
updatedAt: worker.updated_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) { function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
return { return {
workOrderId: Number(workOrder.id), workOrderId: Number(workOrder.id),
@@ -1632,6 +1896,61 @@ function normalizeEnabledStatus(value: unknown) {
return String(value || 'active').trim() === 'disabled' ? 'disabled' : 'active' 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) { function normalizeMatchType(value: unknown) {
const matchType = String(value || '').trim() const matchType = String(value || '').trim()
if (matchType === 'exact') return 'exact' if (matchType === 'exact') return 'exact'
File diff suppressed because it is too large Load Diff
+50 -2
View File
@@ -3,9 +3,12 @@ import type {
CollectLookupResponse, CollectLookupResponse,
UploadedFile, UploadedFile,
WorkOrder, WorkOrder,
WorkerFinanceRequest,
WorkerListResponse, WorkerListResponse,
WorkerLoginResponse, WorkerLoginResponse,
WorkerProfileResponse,
WorkerUser, WorkerUser,
WorkerWalletLedger,
} from '@/types/worker-platform' } from '@/types/worker-platform'
export function registerWorker(payload: { export function registerWorker(payload: {
@@ -37,8 +40,53 @@ export function logoutWorker() {
} }
export function fetchWorkerProfile() { export function fetchWorkerProfile() {
return apiGet<{ worker: WorkerUser; permissions: Record<string, unknown> }>( return apiGet<WorkerProfileResponse>('/api/v1/worker/profile')
'/api/v1/worker/profile', }
export function fetchWorkerWalletLedgers(params?: Record<string, unknown>) {
return apiGet<WorkerListResponse<WorkerWalletLedger>>(
'/api/v1/worker/profile/wallet-ledgers',
params,
)
}
export function fetchWorkerFinanceRequests(params?: Record<string, unknown>) {
return apiGet<WorkerListResponse<WorkerFinanceRequest>>(
'/api/v1/worker/profile/finance-requests',
params,
)
}
export function createWorkerRechargeRequest(payload: {
amount?: number
note?: string
}) {
return apiPost<{ request: WorkerFinanceRequest }>(
'/api/v1/worker/profile/recharge-requests',
payload,
)
}
export function createWorkerWithdrawRequest(payload: {
amount?: number
accountChannel?: string
accountName?: string
accountNo?: string
note?: string
}) {
return apiPost<{ request: WorkerFinanceRequest }>(
'/api/v1/worker/profile/withdraw-requests',
payload,
)
}
export function changeWorkerPassword(payload: {
currentPassword: string
newPassword: string
}) {
return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>(
'/api/v1/worker/profile/change-password',
payload,
) )
} }
+223 -5
View File
@@ -1688,13 +1688,222 @@ select {
color: #f97316; color: #f97316;
} }
.worker-profile-card { .worker-profile-hero {
background: #c8b172; overflow: hidden;
border: 1px solid rgba(25, 94, 125, 0.14);
background:
radial-gradient(circle at top right, rgba(116, 227, 214, 0.18), transparent 30%),
linear-gradient(135deg, #14324a 0%, #1b5d73 55%, #237b77 100%);
} }
.worker-profile-card .ant-statistic-title, .worker-profile-hero .ant-card-body {
.worker-profile-card .ant-statistic-content { display: grid;
color: #fff; gap: 24px;
}
.worker-profile-hero-head {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: flex-start;
}
.worker-profile-identity {
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
}
.worker-profile-avatar {
width: 72px;
height: 72px;
border-radius: 22px;
display: grid;
place-items: center;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.24);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
color: #ffffff;
font-size: 28px;
font-weight: 700;
}
.worker-profile-identity-copy {
display: grid;
gap: 6px;
min-width: 0;
}
.worker-profile-identity-copy .ant-typography {
color: rgba(239, 248, 255, 0.88);
}
.worker-profile-identity-title {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.worker-profile-identity-title .ant-typography {
margin: 0;
color: #ffffff;
}
.worker-profile-tag {
border-color: transparent;
background: rgba(255, 255, 255, 0.14);
color: #f8fafc;
}
.worker-profile-hero-side {
display: flex;
gap: 24px;
flex-wrap: wrap;
justify-content: flex-end;
}
.worker-profile-hero-side .ant-space {
padding: 10px 14px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.1);
}
.worker-profile-hero-side .ant-typography {
color: rgba(239, 248, 255, 0.88);
}
.worker-profile-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
}
.worker-profile-metric {
display: grid;
gap: 6px;
padding: 16px 18px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.worker-profile-metric-label,
.worker-profile-metric-note {
color: rgba(239, 248, 255, 0.75);
font-size: 12px;
}
.worker-profile-metric-value {
color: #ffffff;
font-size: 26px;
line-height: 1.2;
}
.worker-profile-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.worker-profile-action-card {
width: 100%;
padding: 18px;
border-radius: 18px;
border: 1px solid #e2e8f0;
background: #ffffff;
text-align: left;
display: grid;
gap: 10px;
cursor: pointer;
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
border-color 0.18s ease;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04);
}
.worker-profile-action-card:hover {
transform: translateY(-2px);
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.08);
}
.worker-profile-action-card:disabled {
cursor: not-allowed;
opacity: 0.58;
transform: none;
box-shadow: none;
}
.worker-profile-action-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: grid;
place-items: center;
font-size: 20px;
}
.worker-profile-action-title {
color: #0f172a;
font-size: 16px;
font-weight: 600;
}
.worker-profile-action-description {
color: #64748b;
font-size: 13px;
line-height: 1.6;
}
.worker-profile-action-card.is-blue .worker-profile-action-icon {
background: #dbeafe;
color: #2563eb;
}
.worker-profile-action-card.is-green .worker-profile-action-icon {
background: #dcfce7;
color: #16a34a;
}
.worker-profile-action-card.is-slate .worker-profile-action-icon {
background: #e2e8f0;
color: #334155;
}
.worker-profile-action-card.is-orange .worker-profile-action-icon {
background: #ffedd5;
color: #ea580c;
}
.worker-profile-action-card.is-violet .worker-profile-action-icon {
background: #ede9fe;
color: #7c3aed;
}
.worker-profile-action-card.is-amber .worker-profile-action-icon {
background: #fef3c7;
color: #d97706;
}
.worker-profile-action-card.is-rose .worker-profile-action-icon {
background: #ffe4e6;
color: #e11d48;
}
.worker-amount-positive {
color: #047857;
}
.worker-amount-negative {
color: #dc2626;
}
.worker-profile-help-text {
margin: 0;
color: #64748b;
} }
.collect-order-input { .collect-order-input {
@@ -1752,4 +1961,13 @@ select {
.worker-order-grid { .worker-order-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.worker-profile-hero-head {
flex-direction: column;
}
.worker-profile-hero-side {
width: 100%;
justify-content: flex-start;
}
} }
@@ -17,12 +17,56 @@ export type WorkerUser = {
wallet: { wallet: {
availableAmount: number availableAmount: number
frozenDepositAmount: number frozenDepositAmount: number
totalCreditedAmount: number
totalSettledAmount: number
} }
createdAt: string createdAt: string
updatedAt: string updatedAt: string
reviewedAt: string | null reviewedAt: string | null
} }
export type WorkerProfileSummary = {
acceptedOrderCount: number
pendingWithdrawAmount: number
approvedWithdrawAmount: number
pendingRechargeAmount: number
}
export type WorkerWalletLedger = {
ledgerId: number
ledgerType: string
amount: number
balanceAfter: number
frozenAfter: number
relatedWorkOrderId: number | null
auditStatus: string
note: string
payload: Record<string, unknown>
createdAt: string
}
export type WorkerFinanceRequest = {
requestId: number
requestType: string
status: string
amount: number
accountChannel: string
accountName: string
accountNo: string
note: string
reviewedNote: string
payload: Record<string, unknown>
createdAt: string
updatedAt: string
reviewedAt: string | null
}
export type WorkerProfileResponse = {
worker: WorkerUser
permissions: Record<string, unknown>
summary: WorkerProfileSummary
}
export type WorkerLevel = { export type WorkerLevel = {
levelId: number levelId: number
levelKey: string levelKey: string