完善个人中心资金功能

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>
available_amount?: number
frozen_deposit_amount?: number
total_credited_amount?: number
total_settled_amount?: number
}
export type WorkerWalletRow = {
@@ -43,6 +45,37 @@ export type WorkerWalletRow = {
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 = {
id: number
category_key: string
@@ -133,6 +166,21 @@ type ProductRuleListInput = {
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 = {
username: string
passwordHash: string
@@ -167,7 +215,9 @@ const WORKER_USER_SELECT = `
wl.name AS level_name,
wl.permission_json AS level_permission_json,
ww.available_amount,
ww.frozen_deposit_amount
ww.frozen_deposit_amount,
ww.total_credited_amount,
ww.total_settled_amount
FROM worker_users wu
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id
@@ -333,6 +383,30 @@ export async function updateWorkerUser(
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(
workerId: number | string,
now: string,
@@ -394,6 +468,38 @@ export async function getWorkerWallet(workerId: number | string): Promise<Worker
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: {
workerId: 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> {
const result = await query<WorkCategoryRow>(
'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) {
const filters: string[] = []
const params: unknown[] = []
+52
View File
@@ -4,9 +4,14 @@ import { createRateLimitMiddleware, getBodyFieldRateLimitKey } from '../middlewa
import { uploadSingleFile } from './file-upload.js'
import { uploadFileAsset } from '../services/file-storage/file-storage-service.js'
import {
changeWorkerPassword,
createWorkerRechargeRequest,
createWorkerWithdrawRequest,
getWorkerProfile,
getWorkerSessionSummary,
grabWorkerHallOrder,
listWorkerProfileFinanceRequests,
listWorkerProfileWalletLedgers,
listWorkerHallOrders,
listWorkerMyOrders,
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(
'/files/upload',
requireActiveWorker,
@@ -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'
File diff suppressed because it is too large Load Diff
+50 -2
View File
@@ -3,9 +3,12 @@ import type {
CollectLookupResponse,
UploadedFile,
WorkOrder,
WorkerFinanceRequest,
WorkerListResponse,
WorkerLoginResponse,
WorkerProfileResponse,
WorkerUser,
WorkerWalletLedger,
} from '@/types/worker-platform'
export function registerWorker(payload: {
@@ -37,8 +40,53 @@ export function logoutWorker() {
}
export function fetchWorkerProfile() {
return apiGet<{ worker: WorkerUser; permissions: Record<string, unknown> }>(
'/api/v1/worker/profile',
return apiGet<WorkerProfileResponse>('/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;
}
.worker-profile-card {
background: #c8b172;
.worker-profile-hero {
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-card .ant-statistic-content {
color: #fff;
.worker-profile-hero .ant-card-body {
display: grid;
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 {
@@ -1752,4 +1961,13 @@ select {
.worker-order-grid {
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: {
availableAmount: number
frozenDepositAmount: number
totalCreditedAmount: number
totalSettledAmount: number
}
createdAt: string
updatedAt: string
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 = {
levelId: number
levelKey: string