完善个人中心资金功能
This commit is contained in:
@@ -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[] = []
|
||||
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user