Commit 40305b50 authored by 刘斌's avatar 刘斌

fix: 完善消费记录为父子表结构

parent 6ceb318f
......@@ -59,10 +59,10 @@ public class AiConsumeProjectCtrl {
}
/**
* 查询当前筛选条件下的项目总金额合计(跨所有分页的全量结果,口径与列表完全一致)。
* 查询当前筛选条件下的命中明细金额合计与积分合计(跨所有分页的全量结果,口径与列表完全一致)。
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountCny 保留两位小数;无匹配数据时为 0.00)
* @return 合计载体(amountCny 明细金额合计 + consumePoints 明细积分合计,均保留两位小数;无匹配数据时均为 0.00)
*/
@SaCheckPermission("ai:consume:list")
@GetMapping("/summary")
......
......@@ -59,6 +59,11 @@ public class AiConsumeProject extends TenantModel {
*/
private BigDecimal amountCny;
/**
* 消费积分(= Σ 子表明细积分,随明细增删改事务聚合重算)
*/
private BigDecimal consumePoints;
/**
* 每分钟金额(= 项目总金额×60÷成片时长,2位小数HALF_UP;成片时长为0时为NULL;派生计算属 C2)
*/
......
......@@ -13,11 +13,11 @@ import java.time.LocalDate;
* AI工具消费记录扁平导出行视图对象(导出专用,D8)
*
* <p>导出({@code POST /ai/consume/export})将父子表 JOIN 展开为扁平明细行:每行 = 一条明细子行
* + 该项目父级汇总列(所属部门 / 项目名称 / 姓名 / 消费时间 / 项目总金额 / 成片时长 / 每分钟金额 /
* + 该项目父级汇总列(所属部门 / 项目名称 / 姓名 / 消费时间 / 项目总金额 / 消费积分 / 成片时长 / 每分钟金额 /
* 备注)重复展示,其后接明细列(AI工具名称 / 国内外 / 消耗积分 / 金额)。列序与标签对齐 spec
* 「导出末尾合计行」,并保持与既有导出列口径稳定;部门名由 ServiceImpl 批量回填(导出链不触发
* JSON 翻译)。末尾合计行:所属部门 = 「合计」、项目总金额 = 全量命中明细金额合计,其余列(含「金额(人民币)」
* 明细列)留空。成片时长列以分秒文本输出(spec「导出成片时长格式化」)。</p>
* JSON 翻译)。末尾合计行:所属部门 = 「合计」、项目总金额 = 全量命中明细金额合计、消费积分 = 全量命中
* 明细积分合计,其余列(含「金额(人民币)」明细列)留空。成片时长列以分秒文本输出(spec「导出成片时长格式化」)。</p>
*
* <p>非导出辅助字段 {@code deptId} 无 {@code @ExcelProperty},被 {@code @ExcelIgnoreUnannotated}
* 忽略,仅用于批量查询部门名回填 {@code deptName}。两个金额列语义不同:{@code amountCny} 为父级
......@@ -68,6 +68,13 @@ public class AiConsumeExportVo implements Serializable {
@ExcelProperty(value = "项目总金额")
private BigDecimal amountCny;
/**
* 消费积分(父级汇总列,= Σ 子表明细积分,每明细行重复;末尾合计行此列填全量命中明细积分合计;
* 与明细列 {@link #consumePoints}「消耗积分」配对,两者语义不同勿混)
*/
@ExcelProperty(value = "消费积分")
private BigDecimal projectConsumePoints;
/**
* 成片时长(父级汇总列,每明细行重复):导出层格式化为分秒文本({@code >60 秒→「X分Y秒」、
* 1..60 秒→「X秒」、0→「0秒」、NULL→留白}),对齐前端列表 durationText 约定(spec「导出成片时长格式化」)
......
......@@ -69,6 +69,11 @@ public class AiConsumeProjectVo implements Serializable {
*/
private BigDecimal amountCny;
/**
* 消费积分(= Σ 子表明细积分,事务重算冗余存储)
*/
private BigDecimal consumePoints;
/**
* 成片时长(秒)
*/
......
......@@ -24,4 +24,9 @@ public class AiConsumeRecordSummaryVo implements Serializable {
*/
private BigDecimal amountCny;
/**
* 消费积分合计:当前查询条件筛选出的全部命中明细子行积分(跨所有分页)求和,保留两位小数(与 amountCny 同口径)
*/
private BigDecimal consumePoints;
}
......@@ -40,10 +40,10 @@ public interface AiConsumeProjectServ extends IService<AiConsumeProject> {
List<AiConsumeExportVo> queryList(AiConsumeProjectListParam param);
/**
* 查询当前条件下的项目总金额合计(跨所有分页的全量筛选结果,口径与列表完全一致)
* 查询当前条件下的命中明细金额合计与积分合计(跨所有分页的全量筛选结果,口径与列表完全一致)
*
* @param param 查询条件(与分页查询共用同一条件对象 AiConsumeProjectListParam)
* @return 合计载体(amountCny 保留两位小数;无匹配数据时为 0.00)
* @return 合计载体(amountCny 明细金额合计 + consumePoints 明细积分合计,均保留两位小数;无匹配数据时均为 0.00)
*/
AiConsumeRecordSummaryVo querySummary(AiConsumeProjectListParam param);
......
......@@ -23,9 +23,9 @@ export function consumeList(params?: PageQuery) {
}
/**
* 查询消费记录金额合计(跨所有分页的全量筛选结果,口径与列表一致)
* 查询消费记录金额与积分合计(跨所有分页的全量筛选结果,口径与列表一致)
* @param params 请求参数(与列表查询共用同一条件)
* @returns 合计载体(amountCny)
* @returns 合计载体(amountCny 明细金额合计 + consumePoints 明细积分合计)
*/
export function consumeSummary(params?: PageQuery) {
return alovaInstance.get<ConsumeSummary>(Api.consumeSummary, { params });
......
......@@ -30,6 +30,10 @@ export interface Consume {
* 项目总金额(= Σ 明细金额,人民币;列表「项目总金额」列绑定字段)
*/
amountCny: number;
/**
* 消费积分(= Σ 明细积分,列表「消费积分」列绑定字段;存量未回填行为 null 时列留白)
*/
consumePoints?: number;
/**
* 成片时长(秒,非负整数)
*/
......@@ -144,7 +148,11 @@ export interface ConsumeForm {
*/
export interface ConsumeSummary {
/**
* 项目总金额全量合计(两位小数)
* 明细金额合计(命中明细子行金额之和,两位小数)
*/
amountCny: number;
/**
* 明细积分合计(命中明细子行积分之和,两位小数;与 amountCny 同口径)
*/
consumePoints: number;
}
......@@ -30,6 +30,7 @@ import {
TimePicker,
} from 'antdv-next';
import { VxeGrid } from 'vxe-table';
import { cnyText } from './data.ts'
const emit = defineEmits<{ reload: [] }>();
......@@ -186,12 +187,14 @@ const detailColumns: VxeGridProps['columns'] = [
title: '金额(人民币)',
minWidth: 130,
align: 'center',
formatter: ({ cellValue }) => cnyText(cellValue),
editRender: {},
slots: {
edit: (props) => {
const { row, $grid } = props;
return (
<InputNumber
prefix="¥"
class="w-full"
min={0}
onChange={() => $grid?.updateStatus(props)}
......
import type { VxeGridProps } from 'vxe-table';
/**
* 金额格式化:空值留白(合并列中父/子行在对方列天然为 null),有值保留 2 位小数。
* 同时用于父行「项目总金额」与子行「金额(人民币)」列(design D8)
* 数值格式化:空值/空串留白,有值保留 2 位小数。用于父行「消费积分」列(design D8/D5)
*/
function amountText(value?: null | number | string): string {
return value == null || value === '' ? '' : Number(value).toFixed(2);
}
/**
* 人民币金额格式化:空值/空串留白;已带「¥」标识的值直通(幂等,兼容 footer 单元格复用列 formatter),
* 否则补「¥ + 2 位小数」。用于父行「项目总金额」/「每分钟金额」与子行「金额(人民币)」列
* (spec「消费记录列表金额列币种标识」)
*/
export function cnyText(value?: null | number | string): string {
if (value == null || value === '') {
return '';
}
const text = String(value);
return text.startsWith('¥') ? text : `¥${Number(value).toFixed(2)}`;
}
/**
* 成片时长格式化:>60 秒「X分X秒」、≤60 秒「X秒」、0「0秒」;空值(子行)留白(design D8)
*/
......@@ -25,7 +37,7 @@ function durationText(value?: null | number): string {
/**
* 主网格列集(design D7):仅项目父级列 + 展开列。
* - 展开列 type:'expand':点击在该行下方内嵌明细子表(列见 detailColumns,数据取 row.children)
* - 父级列:所属部门 / 项目名称 / 姓名 / 项目总金额(amountCny) / 成片时长 / 每分钟金额 / 消费时间 / 备注 / 操作
* - 父级列:所属部门 / 项目名称 / 姓名 / 项目总金额(amountCny) / 消费积分(consumePoints) / 成片时长 / 每分钟金额 / 消费时间 / 备注 / 操作
* - 明细字段(AI工具名称 / 国内外 / 消耗积分 / 金额)不再占主网格列,移入展开子表
*/
export const columns: VxeGridProps['columns'] = [
......@@ -51,6 +63,13 @@ export const columns: VxeGridProps['columns'] = [
field: 'amountCny',
minWidth: 100,
align: 'right',
formatter: ({ cellValue }) => cnyText(cellValue),
},
{
title: '消费积分',
field: 'consumePoints',
minWidth: 100,
align: 'right',
formatter: ({ cellValue }) => amountText(cellValue),
},
{
......@@ -65,7 +84,7 @@ export const columns: VxeGridProps['columns'] = [
field: 'amountPerMinute',
minWidth: 90,
align: 'right',
formatter: ({ cellValue }) => amountText(cellValue),
formatter: ({ cellValue }) => cnyText(cellValue),
},
{
title: '消费时间',
......@@ -90,7 +109,7 @@ export const columns: VxeGridProps['columns'] = [
/**
* 展开行明细子表列集(design D7 / spec「消费记录列表展开明细子表前端展示」):
* AI工具名称 / 国内外 / 消耗积分 / 金额(人民币)。数据源为父行内嵌 children(ConsumeDetail,
* 金额字段 amountCny);金额列复用 amountText 保留 2 位、空值留白。仅供主网格展开区嵌套只读子表使用。
* 金额字段 amountCny);金额列用 cnyText 补「¥」标识保留 2 位、空值留白。仅供主网格展开区嵌套只读子表使用。
*/
export const detailColumns: VxeGridProps['columns'] = [
{
......@@ -116,6 +135,6 @@ export const detailColumns: VxeGridProps['columns'] = [
field: 'amountCny',
minWidth: 120,
align: 'center',
formatter: ({ cellValue }) => amountText(cellValue),
formatter: ({ cellValue }) => cnyText(cellValue),
},
];
......@@ -75,10 +75,15 @@ const gridOptions = withDefaultVxeGridOptions<Consume>({
}),
consumeSummary(values),
]);
// 合计写入独立 footerData;amountCny 键对齐 data.ts「项目总金额」列 field——
// 合计口径为命中明细金额之和,落「项目总金额」列、其余列留空(design D5,req1);值来源仍是 summary 的 amountCny 单值
// 合计写入独立 footerData;amountCny / consumePoints 键对齐 data.ts「项目总金额」/「消费积分」列 field——
// 合计口径为命中明细金额 / 积分之和,落「项目总金额」与「消费积分」两列、其余列留空(design D3/D5);值来源为 summary 双字段。
// 金额值补「¥」前缀(spec「消费记录列表底部合计行」);积分非金额列不加标识
footerData.value = [
{ deptName: '合计', amountCny: Number(sum.amountCny).toFixed(2) },
{
deptName: '合计',
amountCny: `¥${Number(sum.amountCny).toFixed(2)}`,
consumePoints: Number(sum.consumePoints ?? 0).toFixed(2),
},
];
// 主网格每行 = 一个项目父行,children 原样保留供展开子表直接渲染(design D7);
// 明细移入独立子表实例,父子不再同表格 → 无需子行字段改名 / 复合键 _rid / rowType 判别
......
......@@ -3,6 +3,19 @@ import type { VxeGridProps } from 'vxe-table';
import { DictEnum } from '@/constants';
import { renderDict } from '@/utils/render';
/**
* 币种金额格式化:空值/空串留白;已带对应币种标识的值直通(幂等,兼容 footer 单元格复用列 formatter),
* 否则补「标识 + 原始数值」前缀(不强制补位小数,保持后端返回展示精度)。
* 用于「充值金额(美元)」「充值金额(人民币)」列(spec「充值记录列表金额列币种标识」)
*/
function currencyText(value: null | number | string | undefined, symbol: string): string {
if (value == null || value === '') {
return '';
}
const text = String(value);
return text.startsWith(symbol) ? text : `${symbol}${text}`;
}
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
......@@ -41,10 +54,12 @@ export const columns: VxeGridProps['columns'] = [
{
title: '充值金额(美元)',
field: 'amountUsd',
formatter: ({ cellValue }) => currencyText(cellValue, '$'),
},
{
title: '充值金额(人民币)',
field: 'amountCny',
formatter: ({ cellValue }) => currencyText(cellValue, '¥'),
},
{
title: '开通账号数量',
......
......@@ -73,12 +73,13 @@ const gridOptions = withDefaultVxeGridOptions<Recharge>({
}),
rechargeSummary(values),
]);
// 合计写入独立 footerData;两金额键按 data.ts 列 field 对齐;toFixed(2) 规避 JSON 数值退化(design D6)
// 合计写入独立 footerData;两金额键按 data.ts 列 field 对齐;toFixed(2) 规避 JSON 数值退化(design D6)。
// 合计值补币种标识($ / ¥,spec「充值记录列表底部合计行」)
footerData.value = [
{
deptName: '合计',
amountUsd: Number(sum.amountUsd).toFixed(2),
amountCny: Number(sum.amountCny).toFixed(2),
amountUsd: `$${Number(sum.amountUsd).toFixed(2)}`,
amountCny: `¥${Number(sum.amountCny).toFixed(2)}`,
},
];
return listRes;
......
......@@ -224,6 +224,7 @@ async function handleClosed() {
</FormItem>
<FormItem label="充值金额(美元)" name="amountUsd">
<InputNumber
prefix="$"
class="w-full"
:style="{ width: '50%' }"
:min="0"
......@@ -233,6 +234,7 @@ async function handleClosed() {
</FormItem>
<FormItem label="充值金额(人民币)" name="amountCny" :rules="formRules.amountCny">
<InputNumber
prefix="¥"
class="w-full"
:style="{ width: '50%' }"
:min="0"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment