feat:自动补货/手动采购逻辑修改:批量获取-》打包-》获取Token列表

This commit is contained in:
2026-10-29 16:05:39 +08:00
parent 8198bd6feb
commit 8494fabc82
23 changed files with 3679 additions and 85 deletions
+20
View File
@@ -572,6 +572,7 @@ export interface RockConfig {
bearer_token: string;
username: string;
points: number;
black_token_cost: number;
min_points_warning: number;
token_warning_stock: number;
auto_purchase_enabled: boolean;
@@ -594,15 +595,28 @@ export interface RockStats {
auto_purchase_enabled: boolean;
username: string;
last_heartbeat_at?: string;
last_purchase_error?: string;
last_purchase_error_at?: string;
}
export interface RockDashboard {
available_black: number;
available_blue: number;
black_token_cost: number;
blue_token_cost: number;
cost_updated: boolean;
}
export interface RockTokenItem {
id: number;
upstream_token_id: string;
package_id?: number;
token?: string;
redeem_code: string;
color: string;
region: string;
status: string;
source?: string;
assigned_order_no?: string;
client_order_no?: string;
expected_uid?: string;
@@ -616,10 +630,12 @@ export interface RockTokenItem {
mismatch_at?: string;
mismatch_reason?: string;
remark?: string;
raw_data?: string;
claimed_at?: string;
bound_at?: string;
redeemed_at?: string;
created_at: string;
updated_at?: string;
}
export const rockApi = {
@@ -635,6 +651,10 @@ export const rockApi = {
.then((r) => r.data.data),
getStats: () =>
request.get("/platform/rock/stats").then((r) => r.data.data as RockStats),
refreshDashboard: () =>
request
.post("/platform/rock/dashboard/refresh")
.then((r) => r.data.data as RockDashboard),
purchaseTokens: (count = 50) =>
request.post("/platform/rock/purchase", { count }).then((r) => r.data.data),
listTokens: (params?: {
+93 -4
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import { Button, Form, Modal, Space, message } from 'antd'
import { Alert, Button, Form, Modal, Space, message } from 'antd'
import { ReloadOutlined, ShoppingCartOutlined } from '@ant-design/icons'
import { rockApi, type RockConfig, type RockStats, type RockTokenItem } from '../api'
import { PageHeader } from '../components/PageHeader'
@@ -12,15 +12,28 @@ import { UidEditModal } from './platformRock/UidEditModal'
import { VoucherPreviewModal } from './platformRock/VoucherPreviewModal'
import type { UidEditState } from './platformRock/types'
function prettyRawData(raw?: string) {
if (!raw) return '暂无原始数据'
try {
return JSON.stringify(JSON.parse(raw), null, 2)
} catch {
return raw
}
}
export default function PlatformRock() {
const [config, setConfig] = useState<RockConfig | null>(null)
const [stats, setStats] = useState<RockStats | null>(null)
const [upstreamStock, setUpstreamStock] = useState<number | null>(null)
const [loadingStats, setLoadingStats] = useState(false)
const [testingConn, setTestingConn] = useState(false)
const [savingConfig, setSavingConfig] = useState(false)
const [autoPurchaseEnabled, setAutoPurchaseEnabled] = useState(false)
const [togglingAutoPurchase, setTogglingAutoPurchase] = useState(false)
const [purchasing, setPurchasing] = useState(false)
const [purchaseModalOpen, setPurchaseModalOpen] = useState(false)
const [purchaseCount, setPurchaseCount] = useState(50)
const [rawDataRecord, setRawDataRecord] = useState<RockTokenItem | null>(null)
// 凭证图片操作状态
const [copyingVoucherId, setCopyingVoucherId] = useState<number | null>(null)
@@ -43,16 +56,23 @@ export default function PlatformRock() {
const loadData = useCallback(async () => {
setLoadingStats(true)
try {
try {
const dashboard = await rockApi.refreshDashboard()
setUpstreamStock(dashboard.available_black)
} catch (e) {
setUpstreamStock(null)
message.warning('获取上游库存/采购单价失败: ' + (e instanceof Error ? e.message : '未知错误'))
}
const [cfg, st] = await Promise.all([rockApi.getConfig(), rockApi.getStats()])
setConfig(cfg)
setStats(st)
setAutoPurchaseEnabled(cfg.auto_purchase_enabled)
form.setFieldsValue({
auto_delivery_enabled: cfg.auto_delivery_enabled,
base_url: cfg.base_url,
bearer_token: cfg.bearer_token,
min_points_warning: cfg.min_points_warning,
token_warning_stock: cfg.token_warning_stock,
auto_purchase_enabled: cfg.auto_purchase_enabled,
auto_purchase_count: cfg.auto_purchase_count,
purchase_cooldown_minutes: cfg.purchase_cooldown_minutes,
})
@@ -114,6 +134,24 @@ export default function PlatformRock() {
})
}
// 自动补货开关:切换即保存,避免只改表单值后被刷新回填覆盖
const handleToggleAutoPurchase = async (checked: boolean) => {
const previous = autoPurchaseEnabled
setAutoPurchaseEnabled(checked)
setTogglingAutoPurchase(true)
try {
const updated = await rockApi.updateConfig({ auto_purchase_enabled: checked })
setConfig(updated)
setAutoPurchaseEnabled(updated.auto_purchase_enabled)
message.success(checked ? '自动补货已开启' : '自动补货已关闭')
} catch (e) {
setAutoPurchaseEnabled(previous)
message.error('更新自动补货开关失败: ' + (e instanceof Error ? e.message : '未知错误'))
} finally {
setTogglingAutoPurchase(false)
}
}
// 测试连通性
const handleTestConnection = async () => {
const bearerToken = form.getFieldValue('bearer_token')
@@ -162,6 +200,10 @@ export default function PlatformRock() {
message.warning('请输入有效的采购数量')
return
}
if (upstreamStock !== null && count > upstreamStock) {
message.warning(`上游可用库存不足:当前 ${upstreamStock} 个,本次需要 ${count} 个`)
return
}
setPurchasing(true)
try {
const res = await rockApi.purchaseTokens(count)
@@ -219,8 +261,9 @@ export default function PlatformRock() {
const isStockLow = availableStock <= warningStock
const hasMismatch = (stats?.mismatch_stock ?? 0) > 0
const upstreamPoints = stats?.upstream_points ?? 0
const maxPurchasable = Math.max(0, Math.floor(upstreamPoints / 21))
const estimatedCost = (purchaseCount || 0) * 21
const tokenCost = config?.black_token_cost && config.black_token_cost > 0 ? config.black_token_cost : 20
const maxPurchasable = Math.max(0, Math.floor(upstreamPoints / tokenCost))
const estimatedCost = (purchaseCount || 0) * tokenCost
const isPointsInsufficient = estimatedCost > upstreamPoints
return (
@@ -249,9 +292,23 @@ export default function PlatformRock() {
}
/>
{stats?.last_purchase_error ? (
<Alert
type="warning"
showIcon
style={{ marginBottom: 16 }}
message="最近一次 Rock 采购/打包未完全成功"
description={`${stats.last_purchase_error}${
stats.last_purchase_error_at ? `(${stats.last_purchase_error_at})` : ''
}`}
/>
) : null}
{/* 顶部高质感指标卡片 */}
<RockMetricCards
stats={stats}
upstreamStock={upstreamStock}
blackTokenCost={tokenCost}
availableStock={availableStock}
warningStock={warningStock}
isStockLow={isStockLow}
@@ -265,9 +322,12 @@ export default function PlatformRock() {
<RockStrategyCard
form={form}
autoDelivery={autoDelivery}
autoPurchaseEnabled={autoPurchaseEnabled}
togglingAutoPurchase={togglingAutoPurchase}
testingConn={testingConn}
savingConfig={savingConfig}
onToggleAutoDelivery={handleToggleAutoDelivery}
onToggleAutoPurchase={handleToggleAutoPurchase}
onTestConnection={handleTestConnection}
onSaveConfig={handleSaveConfig}
/>
@@ -296,6 +356,7 @@ export default function PlatformRock() {
setPreviewVoucherUrl={setPreviewVoucherUrl}
setPreviewVoucherOpen={setPreviewVoucherOpen}
onEditUid={setUidEdit}
onViewRawData={setRawDataRecord}
onPageChange={handlePageChange}
onReloadData={loadData}
onReloadTokens={loadTokens}
@@ -311,11 +372,39 @@ export default function PlatformRock() {
upstreamPoints={upstreamPoints}
estimatedCost={estimatedCost}
isPointsInsufficient={isPointsInsufficient}
tokenCost={tokenCost}
upstreamStock={upstreamStock}
onPurchaseCountChange={setPurchaseCount}
onCancel={() => setPurchaseModalOpen(false)}
onConfirm={handlePurchase}
/>
<Modal
open={Boolean(rawDataRecord)}
title={rawDataRecord ? `原始数据 · 0666 #${rawDataRecord.upstream_token_id}` : '原始数据'}
footer={null}
width={760}
onCancel={() => setRawDataRecord(null)}
>
<pre
style={{
maxHeight: 540,
overflow: 'auto',
margin: 0,
padding: 16,
borderRadius: 8,
background: '#0f172a',
color: '#e2e8f0',
fontSize: 12,
lineHeight: 1.6,
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
}}
>
{prettyRawData(rawDataRecord?.raw_data)}
</pre>
</Modal>
<UidEditModal
uidEdit={uidEdit}
savingUid={savingUid}
@@ -11,6 +11,8 @@ export interface PurchaseTokenModalProps {
upstreamPoints: number
estimatedCost: number
isPointsInsufficient: boolean
tokenCost: number
upstreamStock: number | null
onPurchaseCountChange: (value: number) => void
onCancel: () => void
onConfirm: (count: number) => void
@@ -25,10 +27,13 @@ export function PurchaseTokenModal({
upstreamPoints,
estimatedCost,
isPointsInsufficient,
tokenCost,
upstreamStock,
onPurchaseCountChange,
onCancel,
onConfirm,
}: PurchaseTokenModalProps) {
const isStockInsufficient = upstreamStock !== null && purchaseCount > upstreamStock
return (
<Modal
title={
@@ -48,15 +53,16 @@ export function PurchaseTokenModal({
type="primary"
icon={<ShoppingCartOutlined />}
loading={purchasing}
disabled={isPointsInsufficient || purchaseCount <= 0}
disabled={isPointsInsufficient || isStockInsufficient || purchaseCount <= 0}
onClick={() => onConfirm(purchaseCount)}
style={{
background: isPointsInsufficient
? undefined
: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
background:
isPointsInsufficient || isStockInsufficient
? undefined
: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
}}
>
确认采购 {purchaseCount} 个 ({estimatedCost} 点)
确认采购 {purchaseCount} 个 ({estimatedCost.toFixed(2)} 点)
</Button>,
]}
destroyOnClose
@@ -88,12 +94,48 @@ export function PurchaseTokenModal({
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: '#64748b' }}>
<span>采购单价:<strong style={{ color: '#0f172a' }}>21 点 / 个</strong></span>
<span>最大可采购:<strong style={{ color: '#2563eb' }}>{maxPurchasable} 个</strong></span>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 8,
fontSize: 12,
color: '#64748b',
}}
>
<span>
采购单价:<strong style={{ color: '#0f172a' }}>{tokenCost.toFixed(2)} 点 / 个</strong>
</span>
<span>
上游可用库存:
<strong style={{ color: isStockInsufficient ? '#dc2626' : '#0f172a' }}>
{upstreamStock === null ? '未获取' : `${upstreamStock} 个`}
</strong>
</span>
<span>
最大可采购:<strong style={{ color: '#2563eb' }}>{maxPurchasable} 个</strong>
</span>
</div>
</div>
{/* 上游库存不足提示 */}
{isStockInsufficient && (
<div
style={{
background: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: 8,
padding: '10px 16px',
marginBottom: 14,
fontSize: 13,
color: '#b91c1c',
}}
>
⚠️ 上游可用库存不足:当前 {upstreamStock} 个,本次需要 {purchaseCount} 个,请减少采购数量。
</div>
)}
{/* 采购数量输入 */}
<div style={{ marginBottom: 14 }}>
<div style={{ fontWeight: 600, marginBottom: 8, color: '#0f172a' }}>
@@ -153,7 +195,7 @@ export function PurchaseTokenModal({
本次采购消耗积分:
</span>
<strong style={{ color: isPointsInsufficient ? '#dc2626' : '#16a34a', fontSize: 16 }}>
{estimatedCost} 点
{estimatedCost.toFixed(2)} 点
</strong>
</div>
<div
@@ -1,6 +1,7 @@
import { Col, Row, Tag } from 'antd'
import {
CheckCircleOutlined,
CloudServerOutlined,
ExclamationCircleOutlined,
FireOutlined,
ShoppingCartOutlined,
@@ -9,6 +10,8 @@ import type { RockStats } from '../../api'
export interface RockMetricCardsProps {
stats: RockStats | null
upstreamStock: number | null
blackTokenCost: number
availableStock: number
warningStock: number
isStockLow: boolean
@@ -20,6 +23,8 @@ export interface RockMetricCardsProps {
export function RockMetricCards({
stats,
upstreamStock,
blackTokenCost,
availableStock,
warningStock,
isStockLow,
@@ -30,7 +35,38 @@ export function RockMetricCards({
}: RockMetricCardsProps) {
return (
<Row gutter={[16, 16]} style={{ marginBottom: 20 }}>
<Col xs={24} sm={12} lg={6}>
<Col flex="1 1 200px">
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">上游可用库存 (0666.pro)</span>
<div className="metric-card-icon" style={{ background: '#e0f2fe', color: '#0284c7' }}>
<CloudServerOutlined />
</div>
</div>
<div className="metric-card-value" style={{ color: upstreamStock === null ? '#94a3b8' : '#0369a1' }}>
{upstreamStock === null ? '-' : upstreamStock}
<span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>个</span>
</div>
<div className="metric-card-footer">
<span>采购单价: {blackTokenCost.toFixed(2)} 点/个</span>
{upstreamStock === null ? (
<Tag color="warning" style={{ margin: 0, borderRadius: 4 }}>
未获取
</Tag>
) : upstreamStock === 0 ? (
<Tag color="error" style={{ margin: 0, borderRadius: 4 }}>
无库存
</Tag>
) : (
<Tag color="processing" style={{ margin: 0, borderRadius: 4 }}>
实时
</Tag>
)}
</div>
</div>
</Col>
<Col flex="1 1 200px">
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">可用通用库存 (Available)</span>
@@ -45,7 +81,9 @@ export function RockMetricCards({
<div className="metric-card-footer">
<span>警戒水位: {warningStock} 个</span>
{isStockLow ? (
<Tag color="error" style={{ margin: 0, borderRadius: 4 }}>库存紧缺</Tag>
<Tag color="error" style={{ margin: 0, borderRadius: 4 }}>
库存紧缺
</Tag>
) : (
<span style={{ color: '#10b981', fontWeight: 600 }}>储备充足</span>
)}
@@ -53,7 +91,7 @@ export function RockMetricCards({
</div>
</Col>
<Col xs={24} sm={12} lg={6}>
<Col flex="1 1 200px">
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">绑定错误阻断 (Mismatch)</span>
@@ -68,7 +106,9 @@ export function RockMetricCards({
<div className="metric-card-footer">
<span>{hasMismatch ? 'UID强一致校验拦截保护' : '暂无异常阻断'}</span>
{hasMismatch ? (
<Tag color="error" style={{ margin: 0, borderRadius: 4 }}>需核对处理</Tag>
<Tag color="error" style={{ margin: 0, borderRadius: 4 }}>
需核对处理
</Tag>
) : (
<span style={{ color: '#10b981', fontWeight: 600 }}>状态正常</span>
)}
@@ -76,7 +116,7 @@ export function RockMetricCards({
</div>
</Col>
<Col xs={24} sm={12} lg={6}>
<Col flex="1 1 200px">
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">已履约交付 (Redeemed)</span>
@@ -95,7 +135,7 @@ export function RockMetricCards({
</div>
</Col>
<Col xs={24} sm={12} lg={6}>
<Col flex="1 1 200px">
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">0666.pro 账号积分</span>
@@ -27,9 +27,12 @@ const { Text } = Typography
export interface RockStrategyCardProps {
form: FormInstance
autoDelivery: boolean
autoPurchaseEnabled: boolean
togglingAutoPurchase: boolean
testingConn: boolean
savingConfig: boolean
onToggleAutoDelivery: (checked: boolean) => void
onToggleAutoPurchase: (checked: boolean) => void
onTestConnection: () => void
onSaveConfig: () => void
}
@@ -37,9 +40,12 @@ export interface RockStrategyCardProps {
export function RockStrategyCard({
form,
autoDelivery,
autoPurchaseEnabled,
togglingAutoPurchase,
testingConn,
savingConfig,
onToggleAutoDelivery,
onToggleAutoPurchase,
onTestConnection,
onSaveConfig,
}: RockStrategyCardProps) {
@@ -227,13 +233,24 @@ export function RockStrategyCard({
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="auto_purchase_enabled" label="自动补货触发开关" valuePropName="checked">
<Switch checkedChildren="开启自动补货" unCheckedChildren="关闭" />
<Form.Item label="自动补货触发开关">
<Space direction="vertical" size={2}>
<Switch
checked={autoPurchaseEnabled}
onChange={onToggleAutoPurchase}
loading={togglingAutoPurchase}
checkedChildren="开启自动补货"
unCheckedChildren="关闭"
/>
<Text type="secondary" style={{ fontSize: 11 }}>
切换后立即保存
</Text>
</Space>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="auto_purchase_count" label="单次自动补货数量">
<InputNumber min={5} max={200} style={{ width: '100%' }} addonAfter="个" />
<InputNumber min={1} max={200} style={{ width: '100%' }} addonAfter="个" />
</Form.Item>
</Col>
<Col span={12}>
@@ -34,12 +34,12 @@ export function RockTokenFilterBar({
<Col xs={24} md={16}>
<Space size="middle" wrap>
<Input
placeholder="搜索 0666 编号 / 提取码 / 订单号 / UID"
placeholder="搜索 包ID / 0666 编号 / 提取码 / 兑换码 / 订单号 / UID"
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
value={searchKeyword}
onChange={(e) => onSearchKeywordChange(e.target.value)}
onPressEnter={onRefresh}
style={{ width: 340 }}
style={{ width: 400 }}
allowClear
/>
<Radio.Group
@@ -72,7 +72,7 @@ export function RockTokenFilterBar({
</Col>
<Col xs={24} md={8} style={{ textAlign: 'right' }}>
<Text type="secondary" style={{ fontSize: 13 }}>
共检索到 <strong style={{ color: '#0f172a' }}>{totalTokens}</strong> 条库存记录
共检索到 <strong style={{ color: '#0f172a' }}>{totalTokens}</strong> 条 Token 记录
</Text>
</Col>
</Row>
@@ -18,6 +18,7 @@ export interface RockTokenTableProps {
setPreviewVoucherUrl: Dispatch<SetStateAction<string>>
setPreviewVoucherOpen: Dispatch<SetStateAction<boolean>>
onEditUid: (state: UidEditState) => void
onViewRawData: (record: RockTokenItem) => void
onPageChange: (page: number, pageSize: number) => void
onReloadData: () => void
onReloadTokens: () => void
@@ -34,6 +35,7 @@ export function RockTokenTable({
setPreviewVoucherUrl,
setPreviewVoucherOpen,
onEditUid,
onViewRawData,
onPageChange,
onReloadData,
onReloadTokens,
@@ -153,6 +155,7 @@ export function RockTokenTable({
onCopyVoucher: handleCopyVoucher,
onPreviewVoucher: handlePreviewVoucher,
onEditUid,
onViewRawData,
onResetToken: handleResetToken,
onReleaseToken: handleReleaseToken,
onUpdateRemark: handleUpdateRemark,
@@ -165,14 +168,14 @@ export function RockTokenTable({
columns={columns}
dataSource={tokens}
loading={loadingTokens}
scroll={{ x: 1300 }}
scroll={{ x: 1700 }}
pagination={{
current: page,
pageSize,
total: totalTokens,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total) => `共 ${total} 条库存记录`,
showTotal: (total) => `共 ${total} 条 Token 记录`,
onChange: (p, ps) => {
onPageChange(p, ps)
},
@@ -13,6 +13,7 @@ export interface TokenColumnHandlers {
onCopyVoucher: (record: RockTokenItem) => void
onPreviewVoucher: (record: RockTokenItem) => void
onEditUid: (state: UidEditState) => void
onViewRawData: (record: RockTokenItem) => void
onResetToken: (id: number) => void
onReleaseToken: (id: number) => void
onUpdateRemark: (id: number, remark: string, status?: string) => void
@@ -24,6 +25,7 @@ export function buildTokenColumns({
onCopyVoucher,
onPreviewVoucher,
onEditUid,
onViewRawData,
onResetToken,
onReleaseToken,
onUpdateRemark,
@@ -31,7 +33,7 @@ export function buildTokenColumns({
}: TokenColumnHandlers): ColumnsType<RockTokenItem> {
return [
{
title: '0666 编号 / 提取码',
title: '0666 编号 / 兑换码',
dataIndex: 'upstream_token_id',
width: 250,
render: (_, record) => (
@@ -53,6 +55,32 @@ export function buildTokenColumns({
</Space>
),
},
{
title: '包ID / 提取码',
width: 230,
render: (_, record) => (
<Space direction="vertical" size={2}>
<div style={{ fontSize: 12, color: '#334155' }}>
<span style={{ color: '#94a3b8' }}>包ID: </span>
{record.package_id ? `#${record.package_id}` : '-'}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ color: '#94a3b8', fontSize: 12 }}>提取码:</span>
<Text
copyable={record.token ? { text: record.token } : false}
style={{ fontSize: 12, fontFamily: 'monospace', fontWeight: 600 }}
>
{record.token || '-'}
</Text>
</div>
{record.source === 'package' ? (
<Tag color="purple" style={{ margin: 0, width: 'fit-content', fontSize: 10 }}>
打包导入
</Tag>
) : null}
</Space>
),
},
{
title: '规格与适用范围',
dataIndex: 'color',
@@ -338,6 +366,18 @@ export function buildTokenColumns({
)
},
},
{
title: '原始数据',
width: 100,
render: (_, record) =>
record.raw_data ? (
<Button size="small" onClick={() => onViewRawData(record)}>
查看
</Button>
) : (
<Text type="secondary">-</Text>
),
},
{
title: '操作',
key: 'action',