后台充值订单实付口径、发放泥点列、用户累计充值与兑换码单位
Project CI / AI game creator shell Rust smoke (push) Successful in 2m17s
Project CI / Backend tests (push) Successful in 4m59s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m29s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m40s
Project CI / Frontend tests (push) Successful in 1m59s
Project CI / AI game creator shell Rust crates (push) Successful in 9m16s
Project CI / AI game creator shell web tests (push) Successful in 1m41s
Project CI / Repository checks (push) Successful in 4m4s
Project CI / Native shell tests (push) Successful in 7m1s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m17s
Project CI / Backend tests (push) Successful in 4m59s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m29s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m40s
Project CI / Frontend tests (push) Successful in 1m59s
Project CI / AI game creator shell Rust crates (push) Successful in 9m16s
Project CI / AI game creator shell web tests (push) Successful in 1m41s
Project CI / Repository checks (push) Successful in 4m4s
Project CI / Native shell tests (push) Successful in 7m1s
- 新增 AdminRechargeOrderEntryPayload.paidAmountCents:只有 paid_at 存在的订单才有实付,未支付 / 已关闭 / 已过期固定为 0 - 充值管理列表把「金额 / 泥点」拆成「实付」「发放泥点」两列,未支付行实付显示「未支付」并附订单金额小字;退款面板「订单实付」改读同一字段 - 用户详情充值订单表新增「发放泥点」列,商品列只保留商品名,实付同样按 paidAmountCents 展示 - 用户详情新增 cumulativeRechargedCents:api-server 按 user_id 读取 profile_recharge_order,只累加 paid_at 存在的订单金额(退款不回减),单次上限 500 行,读取失败或命中上限返回 null - 用户详情身份区新增「累计充值」,读不到时显示「未知」,不用用户详情最多 20 条订单在 BFF 或前端近似重算 - 兑换码奖励单位收口为泥点:输入标签与列表列头改「奖励泥点」,单元格带「泥点」单位,避免被当成元 - 新增后端 2 条累计充值口径单测与前端 2 条用例(未支付不显示实付且发放泥点独立成列、累计充值未知态) - 同步后端架构数据契约(实付口径 / 累计充值来源与上限 / 兑换码奖励单位)与决策记录 - 验证:cargo check -p api-server;cargo test -p api-server --bin api-server admin(138 passed / 0 failed / 1 ignored);npm run admin-web:typecheck;npx vitest run apps/admin-web/src(220 passed);cargo fmt --all --check、check:encoding、check:doc-index、git diff --check 通过
This commit is contained in:
@@ -958,6 +958,8 @@ export interface AdminRechargeOrderEntryPayload {
|
||||
productTitle: string;
|
||||
productKind: string;
|
||||
amountCents: number;
|
||||
/** 真实支付金额(分):未支付 / 已关闭 / 已过期订单固定为 0,不能拿订单金额当实付。 */
|
||||
paidAmountCents: number;
|
||||
status: string;
|
||||
paymentChannel: string;
|
||||
paidAtMicros?: number | null;
|
||||
@@ -997,6 +999,8 @@ export interface AdminUserDetailResponse {
|
||||
phoneBound: boolean;
|
||||
wechatBound: boolean;
|
||||
historicalConsumedPoints: number;
|
||||
/** 累计充值金额(分):读取失败或命中读取上限时为 null,前端按未知展示。 */
|
||||
cumulativeRechargedCents?: number | null;
|
||||
canReconcileConsumption: boolean;
|
||||
wallet: AdminProfileWalletPayload;
|
||||
rechargeOrders: AdminRechargeOrderEntryPayload[];
|
||||
|
||||
@@ -51,6 +51,7 @@ const detail: AdminUserDetailResponse = {
|
||||
phoneBound: true,
|
||||
wechatBound: true,
|
||||
historicalConsumedPoints: 1234,
|
||||
cumulativeRechargedCents: 128800,
|
||||
canReconcileConsumption: true,
|
||||
wallet,
|
||||
rechargeOrders: [
|
||||
@@ -62,6 +63,7 @@ const detail: AdminUserDetailResponse = {
|
||||
productTitle: '60泥点',
|
||||
productKind: 'points',
|
||||
amountCents: 600,
|
||||
paidAmountCents: 600,
|
||||
status: 'paid',
|
||||
paymentChannel: 'wechat_native',
|
||||
paidAtMicros: 1_720_000_000_000_000,
|
||||
@@ -124,7 +126,11 @@ test('用户查看按钮按内部 ID 查询并展示脱敏资料、余额与退
|
||||
expect(screen.getByText('25', { selector: 'strong' })).toBeTruthy();
|
||||
expect(screen.getByText('历史花费')).toBeTruthy();
|
||||
expect(screen.getByText('1234', { selector: 'strong' })).toBeTruthy();
|
||||
expect(screen.getByText('累计充值')).toBeTruthy();
|
||||
expect(screen.getByText('¥1288.00')).toBeTruthy();
|
||||
expect(screen.getByText('order-1')).toBeTruthy();
|
||||
expect(screen.getByRole('columnheader', { name: '实付' })).toBeTruthy();
|
||||
expect(screen.getByRole('columnheader', { name: '发放泥点' })).toBeTruthy();
|
||||
|
||||
await user.keyboard('{Escape}');
|
||||
await waitFor(() =>
|
||||
@@ -133,6 +139,29 @@ test('用户查看按钮按内部 ID 查询并展示脱敏资料、余额与退
|
||||
await waitFor(() => expect(document.activeElement).toBe(trigger));
|
||||
});
|
||||
|
||||
test('累计充值读取不到时展示未知,不用订单列表近似', async () => {
|
||||
vi.mocked(getAdminUserDetail).mockResolvedValue({
|
||||
...detail,
|
||||
cumulativeRechargedCents: null,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdminUserReferenceButton
|
||||
token="admin-token"
|
||||
userId="user-1"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '查看用户信息' }));
|
||||
await screen.findByText('陶泥用户');
|
||||
|
||||
expect(screen.getByText('累计充值')).toBeTruthy();
|
||||
expect(screen.getByText('累计充值').nextElementSibling?.textContent).toBe(
|
||||
'未知',
|
||||
);
|
||||
});
|
||||
|
||||
test('只有陶泥号时按 publicUserCode 查询用户', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
|
||||
@@ -364,6 +364,7 @@ export function AdminUserDetailDialog({
|
||||
<th>订单</th>
|
||||
<th>商品</th>
|
||||
<th>实付</th>
|
||||
<th>发放泥点</th>
|
||||
<th>退款</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
@@ -377,11 +378,13 @@ export function AdminUserDetailDialog({
|
||||
</span>
|
||||
<small>{formatMicros(order.createdAtMicros)}</small>
|
||||
</td>
|
||||
<td>{order.productTitle || order.productId}</td>
|
||||
<td>
|
||||
{order.productTitle || order.productId}
|
||||
<small>发放 {order.pointsDelta} 泥点</small>
|
||||
{order.paidAmountCents > 0
|
||||
? formatMoney(order.paidAmountCents)
|
||||
: '未支付'}
|
||||
</td>
|
||||
<td>{formatMoney(order.amountCents)}</td>
|
||||
<td>{order.pointsDelta} 泥点</td>
|
||||
<td>
|
||||
{formatMoney(order.cumulativeSuccessRefundCents)}
|
||||
<small>欠账 {order.unrecoveredPoints} 泥点</small>
|
||||
@@ -435,6 +438,14 @@ function UserIdentityHeader({ detail }: { detail: AdminUserDetailResponse }) {
|
||||
<dt>登录方式</dt>
|
||||
<dd>{detail.loginMethod || '-'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>累计充值</dt>
|
||||
<dd>
|
||||
{typeof detail.cumulativeRechargedCents === 'number'
|
||||
? formatMoney(detail.cumulativeRechargedCents)
|
||||
: '未知'}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>绑定状态</dt>
|
||||
<dd>
|
||||
|
||||
@@ -64,6 +64,7 @@ const baseOrder: AdminRechargeOrderEntryPayload = {
|
||||
productTitle: '60泥点',
|
||||
productKind: 'points',
|
||||
amountCents: 600,
|
||||
paidAmountCents: 600,
|
||||
status: 'paid',
|
||||
paymentChannel: 'wechat_native',
|
||||
paidAtMicros: 1_720_000_000_000_000,
|
||||
@@ -129,6 +130,40 @@ beforeEach(() => {
|
||||
);
|
||||
});
|
||||
|
||||
test('未支付订单不显示实付金额,发放泥点单独成列', async () => {
|
||||
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
|
||||
entries: [
|
||||
{
|
||||
...baseOrder,
|
||||
orderId: 'order-pending',
|
||||
status: 'pending',
|
||||
paidAtMicros: null,
|
||||
paidAmountCents: 0,
|
||||
pointsDelta: 0,
|
||||
},
|
||||
{ ...baseOrder, orderId: 'order-paid' },
|
||||
],
|
||||
});
|
||||
renderPage();
|
||||
|
||||
expect(
|
||||
await screen.findByRole('columnheader', { name: '实付' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('columnheader', { name: '发放泥点' })).toBeTruthy();
|
||||
|
||||
const unpaidRow = (await screen.findByText('order-pending')).closest(
|
||||
'tr',
|
||||
) as HTMLElement;
|
||||
const unpaidCells = within(unpaidRow).getAllByRole('cell');
|
||||
expect(unpaidCells[3]?.textContent).toContain('未支付');
|
||||
expect(unpaidCells[4]?.textContent).toBe('0 泥点');
|
||||
|
||||
const paidRow = screen.getByText('order-paid').closest('tr') as HTMLElement;
|
||||
const paidCells = within(paidRow).getAllByRole('cell');
|
||||
expect(paidCells[3]?.textContent).toBe('¥6.00');
|
||||
expect(paidCells[4]?.textContent).toBe('60 泥点');
|
||||
});
|
||||
|
||||
test('充值订单查询传递全部筛选字段', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
|
||||
@@ -658,7 +658,8 @@ export function AdminRechargeOrderPage({
|
||||
<th>用户</th>
|
||||
<th>订单</th>
|
||||
<th>支付</th>
|
||||
<th>金额 / 泥点</th>
|
||||
<th>实付</th>
|
||||
<th>发放泥点</th>
|
||||
<th>退款与追回</th>
|
||||
<th>钱包</th>
|
||||
<th>状态</th>
|
||||
@@ -715,7 +716,7 @@ export function AdminRechargeOrderPage({
|
||||
<div className="admin-refund-summary-grid">
|
||||
<Metric
|
||||
label="订单实付"
|
||||
value={formatMoney(refundOrder.amountCents)}
|
||||
value={formatMoney(refundOrder.paidAmountCents)}
|
||||
/>
|
||||
<Metric
|
||||
label="累计已退"
|
||||
@@ -1118,8 +1119,13 @@ function RechargeOrderRow({
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{formatMoney(order.amountCents)}</strong>
|
||||
<small>发放 {order.pointsDelta} 泥点</small>
|
||||
<strong>{formatOrderPaidAmount(order)}</strong>
|
||||
{order.paidAmountCents > 0 ? null : (
|
||||
<small>订单 {formatMoney(order.amountCents)}</small>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span>{order.pointsDelta} 泥点</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>累计 {formatMoney(order.cumulativeSuccessRefundCents)}</span>
|
||||
@@ -1312,6 +1318,13 @@ function formatMoney(cents: number) {
|
||||
return `¥${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** 实付只属于真正支付过的订单:未支付 / 已关闭 / 已过期订单显示“未支付”。 */
|
||||
function formatOrderPaidAmount(order: AdminRechargeOrderEntryPayload) {
|
||||
return order.paidAmountCents > 0
|
||||
? formatMoney(order.paidAmountCents)
|
||||
: '未支付';
|
||||
}
|
||||
|
||||
function formatCentsInput(cents: number) {
|
||||
return (cents / 100).toFixed(2);
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ export function AdminRedeemCodePage({
|
||||
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field">
|
||||
<span>奖励点数</span>
|
||||
<span>奖励泥点</span>
|
||||
<input
|
||||
min={1}
|
||||
step={1}
|
||||
@@ -319,7 +319,7 @@ export function AdminRedeemCodePage({
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>奖励</th>
|
||||
<th>奖励泥点</th>
|
||||
<th>状态</th>
|
||||
<th>有效期</th>
|
||||
</tr>
|
||||
@@ -337,7 +337,7 @@ export function AdminRedeemCodePage({
|
||||
</button>
|
||||
<small>{redeemModeLabel(entry.mode)}</small>
|
||||
</td>
|
||||
<td>{entry.rewardPoints}</td>
|
||||
<td>{entry.rewardPoints} 泥点</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-status ${redeemValidityClass(entry)}`}
|
||||
|
||||
Reference in New Issue
Block a user