Compare commits

...

2 Commits

18 changed files with 369 additions and 118 deletions

View File

@ -32,12 +32,46 @@
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['ai:data:export']"
>导出</el-button>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-row :gutter="10" class="mb8"> <p class="summary-line">
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <template v-if="summaryState.mode === 'loading'">
</el-row> <span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="summary-prefix">汇总</span><span class="summary-muted">加载中</span>
</template>
<template v-else-if="summaryState.mode === 'empty'">
<span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="summary-prefix">汇总</span><span class="summary-muted"></span>
</template>
<template v-else>
<span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="summary-prefix">汇总</span>
<span class="summary-label">总计</span>
<span class="summary-num">{{ summaryState.totalStr }}</span>
<span class="summary-label">条记录实际充值积分</span>
<span class="summary-num">{{ summaryState.recharge }}</span>
<span class="summary-unit"></span>
<span class="summary-label">消耗积分</span>
<span class="summary-num">{{ summaryState.score }}</span>
<span class="summary-unit"></span>
<span class="summary-label">实际订单数量成功</span>
<span class="summary-num">{{ summaryState.orders }}</span>
<span class="summary-unit"></span>
<span class="summary-label">三方消耗tokens数量</span>
<span class="summary-num">{{ summaryState.tokens }}</span>
<span class="summary-unit"></span>
</template>
</p>
<el-table v-loading="loading" :data="dataList"> <el-table v-loading="loading" :data="dataList">
<el-table-column label="日期" align="center" prop="dateKey" min-width="110" /> <el-table-column label="日期" align="center" prop="dateKey" min-width="110" />
@ -90,6 +124,7 @@ export default {
showSearch: true, showSearch: true,
total: 0, total: 0,
dataList: [], dataList: [],
aggregate: null,
deptOptions: [], deptOptions: [],
dateRange: [], dateRange: [],
queryParams: { queryParams: {
@ -107,9 +142,30 @@ export default {
this.syncDateRangeToQuery() this.syncDateRangeToQuery()
this.getList() this.getList()
}, },
computed: {
summaryState() {
if (this.loading) {
return { mode: "loading" }
}
const a = this.aggregate
if (!a) {
return { mode: "empty" }
}
const fmt = (v) => this.formatWesternNumberValue(v)
const n = a.totalRows != null ? Number(a.totalRows) : 0
const totalStr = Number.isNaN(n) ? "0" : n.toLocaleString("en-US")
return {
mode: "ok",
totalStr,
recharge: fmt(a.sumRechargeScore),
score: fmt(a.sumScore),
orders: fmt(a.sumOrderCount),
tokens: fmt(a.sumUseTokens)
}
}
},
methods: { methods: {
/** null/空 显示 0数字按 en-US 千分位(如 99,999,999 */ formatWesternNumberValue(cellValue) {
formatWesternNumber(row, column, cellValue) {
if (cellValue === null || cellValue === undefined || cellValue === "") { if (cellValue === null || cellValue === undefined || cellValue === "") {
return (0).toLocaleString("en-US") return (0).toLocaleString("en-US")
} }
@ -119,6 +175,10 @@ export default {
} }
return n.toLocaleString("en-US") return n.toLocaleString("en-US")
}, },
/** null/空 显示 0数字按 en-US 千分位(如 99,999,999 */
formatWesternNumber(row, column, cellValue) {
return this.formatWesternNumberValue(cellValue)
},
buildLastMonthDateRange() { buildLastMonthDateRange() {
const end = new Date() const end = new Date()
const start = new Date(end.getTime()) const start = new Date(end.getTime())
@ -160,8 +220,10 @@ export default {
this.loading = true this.loading = true
listData(this.queryParams) listData(this.queryParams)
.then(response => { .then(response => {
this.dataList = response.rows this.dataList = response.rows || []
this.total = response.total this.total = response.total != null ? response.total : 0
this.aggregate =
response.param && response.param.aggregate != null ? response.param.aggregate : null
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false
@ -177,7 +239,57 @@ export default {
this.queryParams.pageNum = 1 this.queryParams.pageNum = 1
this.syncDateRangeToQuery() this.syncDateRangeToQuery()
this.getList() this.getList()
},
handleExport() {
this.syncDateRangeToQuery()
this.download(
"ai/data/export",
{
startDate: this.queryParams.startDate,
endDate: this.queryParams.endDate,
deptId: this.queryParams.deptId
},
`team_consume_${new Date().getTime()}.xlsx`
)
} }
} }
} }
</script> </script>
<style scoped>
.summary-line {
color: #606266;
font-size: 13px;
margin: 0 0 12px 0;
padding-left: 8px;
line-height: 1.75;
}
.summary-prefix {
color: #909399;
font-weight: 600;
margin-right: 2px;
}
.summary-muted {
color: #c0c4cc;
}
.summary-label {
color: #606266;
}
.summary-unit {
color: #909399;
font-size: 12px;
margin-left: 1px;
margin-right: 1px;
}
.summary-num {
color: #409eff;
font-weight: 600;
font-variant-numeric: tabular-nums;
margin: 0 2px;
}
</style>

View File

@ -19,7 +19,12 @@
</el-form-item> </el-form-item>
<el-form-item label="类型" prop="orderType"> <el-form-item label="类型" prop="orderType">
<el-select v-model="queryParams.orderType" placeholder="全部" clearable style="width: 110px"> <el-select v-model="queryParams.orderType" placeholder="全部" clearable style="width: 110px">
<el-option v-for="item in orderTypeOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option
v-for="item in orderTypeQueryOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="创建时间"> <el-form-item label="创建时间">
@ -151,6 +156,10 @@ export default {
amount: [{ required: true, message: "请填写积分", trigger: "blur" }], amount: [{ required: true, message: "请填写积分", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "change" }] status: [{ required: true, message: "请选择状态", trigger: "change" }]
}, },
orderTypeQueryOptions: [
{ label: "充值", value: 0 },
{ label: "退款", value: 1 }
],
orderTypeOptions: [ orderTypeOptions: [
{ label: "充值", value: 0 }, { label: "充值", value: 0 },
{ label: "退款", value: 1 }, { label: "退款", value: 1 },

View File

@ -8,19 +8,12 @@
v-show="showSearch" v-show="showSearch"
label-width="68px" label-width="68px"
> >
<el-form-item label="主键ID" prop="id">
<el-input
v-model="queryParams.id"
placeholder="请输入主键ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="用户ID" prop="userId"> <el-form-item label="用户ID" prop="userId">
<el-input <el-input
v-model="queryParams.userId" v-model="queryParams.userId"
placeholder="请输入用户ID" placeholder="请输入用户ID"
clearable clearable
style="width: 150px"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
@ -29,19 +22,12 @@
v-model="queryParams.username" v-model="queryParams.username"
placeholder="请输入用户账号" placeholder="请输入用户账号"
clearable clearable
style="width: 150px"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="上级ID" prop="superisuperiorUuidorId"> <el-form-item label="状态" prop="status" label-width="40px">
<el-input <el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 80px">
v-model="queryParams.superiorUuid"
placeholder="请输入上级ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 240px">
<el-option <el-option
v-for="dict in dict.type.sys_normal_disable" v-for="dict in dict.type.sys_normal_disable"
:key="dict.value" :key="dict.value"
@ -50,7 +36,7 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="归属部门" prop="deptId"> <el-form-item label="归属团队" prop="deptId">
<treeselect <treeselect
v-model="queryParams.deptId" v-model="queryParams.deptId"
:options="deptOptions" :options="deptOptions"
@ -63,11 +49,6 @@
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button <el-button
type="primary" type="primary"
plain plain
@ -76,8 +57,6 @@
@click="handleAdd" @click="handleAdd"
v-hasPermi="['ai:user:add']" v-hasPermi="['ai:user:add']"
>新增</el-button> >新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button <el-button
type="danger" type="danger"
plain plain
@ -87,8 +66,6 @@
@click="handleDelete()" @click="handleDelete()"
v-hasPermi="['ai:user:remove']" v-hasPermi="['ai:user:remove']"
>删除</el-button> >删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button <el-button
type="warning" type="warning"
plain plain
@ -97,13 +74,13 @@
@click="handleExport" @click="handleExport"
v-hasPermi="['ai:user:export']" v-hasPermi="['ai:user:export']"
>导出</el-button> >导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-form-item>
</el-form>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="50" align="center" />
<el-table-column label="主键ID" align="center" prop="id" width="90" /> <el-table-column label="主键ID" align="center" prop="id" width="60" />
<el-table-column label="用户ID" align="center" prop="userId" /> <el-table-column label="用户ID" align="center" prop="userId" />
<el-table-column label="用户账号" align="center" prop="username" /> <el-table-column label="用户账号" align="center" prop="username" />
<!-- <!--
@ -111,7 +88,7 @@
<el-table-column label="上级账号" align="center" prop="superiorName" /> <el-table-column label="上级账号" align="center" prop="superiorName" />
--> -->
<el-table-column label="用户昵称" align="center" prop="nickname" /> <el-table-column label="用户昵称" align="center" prop="nickname" />
<el-table-column label="归属部门" align="center" prop="deptName" width="120" show-overflow-tooltip /> <el-table-column label="归属团队" align="center" prop="deptName" width="120" show-overflow-tooltip />
<!-- <!--
<el-table-column label="邮箱" align="center" prop="email" /> <el-table-column label="邮箱" align="center" prop="email" />
<el-table-column label="性别" align="center" prop="gender"> <el-table-column label="性别" align="center" prop="gender">
@ -121,7 +98,7 @@
</template> </template>
</el-table-column> </el-table-column>
--> -->
<el-table-column label="状态" align="center" key="status"> <el-table-column label="状态" align="center" key="status" width="60">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch <el-switch
v-model="scope.row.status" v-model="scope.row.status"
@ -136,11 +113,10 @@
--> -->
<el-table-column label="登录时间" align="center" prop="loginTime" width="150"> <el-table-column label="登录时间" align="center" prop="loginTime" width="150">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ parseTime(scope.row.loginTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.loginTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="剩余积分" align="center" prop="balance" /> <el-table-column label="剩余积分" align="center" prop="balance" />
<el-table-column label="source" align="center" prop="source" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="460"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="460">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button
@ -149,7 +125,7 @@
icon="el-icon-office-building" icon="el-icon-office-building"
@click="handleOpenAssignDept(scope.row)" @click="handleOpenAssignDept(scope.row)"
v-hasPermi="['ai:user:edit']" v-hasPermi="['ai:user:edit']"
>分配部门</el-button> >分配团队</el-button>
<el-button <el-button
size="mini" size="mini"
type="text" type="text"
@ -269,15 +245,15 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 分配部门 --> <!-- 分配团队 -->
<el-dialog title="分配归属部门" :visible.sync="assignDeptOpen" width="480px" append-to-body @close="cancelAssignDept"> <el-dialog title="分配归属团队" :visible.sync="assignDeptOpen" width="480px" append-to-body @close="cancelAssignDept">
<el-form label-width="88px"> <el-form label-width="88px">
<el-form-item label="归属部门"> <el-form-item label="归属团队">
<treeselect <treeselect
v-model="assignForm.deptId" v-model="assignForm.deptId"
:options="deptOptions" :options="deptOptions"
:normalizer="deptNormalizer" :normalizer="deptNormalizer"
placeholder="不选则不归属任何部门" placeholder="不选则不归属任何团队"
clearable clearable
/> />
</el-form-item> </el-form-item>
@ -297,6 +273,9 @@
@close="cancelDeptScore" @close="cancelDeptScore"
> >
<el-form ref="deptScoreFormRef" :model="deptScoreForm" :rules="deptScoreRules" label-width="88px"> <el-form ref="deptScoreFormRef" :model="deptScoreForm" :rules="deptScoreRules" label-width="88px">
<el-form-item label="所属团队">
<span>{{ deptScoreForm.deptName || "—" }}</span>
</el-form-item>
<el-form-item label="用户账号"> <el-form-item label="用户账号">
<span>{{ deptScoreForm.username }}</span> <span>{{ deptScoreForm.username }}</span>
</el-form-item> </el-form-item>
@ -392,6 +371,7 @@ export default {
deptScoreForm: { deptScoreForm: {
userId: null, userId: null,
username: "", username: "",
deptName: "",
amount: undefined, amount: undefined,
remark: "" remark: ""
}, },
@ -526,13 +506,14 @@ export default {
}, },
openDeptScoreDialog(row, mode) { openDeptScoreDialog(row, mode) {
if (!row.deptId) { if (!row.deptId) {
this.$modal.msgWarning("请先分配归属部门后再操作积分"); this.$modal.msgWarning("请先分配归属团队后再操作积分");
return; return;
} }
this.deptScoreMode = mode; this.deptScoreMode = mode;
this.deptScoreForm = { this.deptScoreForm = {
userId: row.id, userId: row.id,
username: row.username || row.userId || "", username: row.username || row.userId || "",
deptName: row.deptName || "",
amount: undefined, amount: undefined,
remark: "" remark: ""
}; };
@ -569,7 +550,7 @@ export default {
}, },
cancelDeptScore() { cancelDeptScore() {
this.deptScoreOpen = false; this.deptScoreOpen = false;
this.deptScoreForm = { userId: null, username: "", amount: undefined, remark: "" }; this.deptScoreForm = { userId: null, username: "", deptName: "", amount: undefined, remark: "" };
}, },
// updateBalance(row) { // updateBalance(row) {
// this.reset(); // this.reset();

View File

@ -3,7 +3,8 @@
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="88px"> <el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="88px">
<el-form-item label="类型" prop="orderType"> <el-form-item label="类型" prop="orderType">
<el-select v-model="queryParams.orderType" clearable placeholder="全部"> <el-select v-model="queryParams.orderType" clearable placeholder="全部">
<el-option label="充值" :value="0" /><el-option label="退款" :value="1" /><el-option label="手动修改" :value="2" /> <el-option label="充值" :value="0" />
<el-option label="退款" :value="1" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="订单编号" prop="orderNum"> <el-form-item label="订单编号" prop="orderNum">

View File

@ -13,7 +13,7 @@
<el-card shadow="hover"><div class="metric-title">团队名称</div><div class="metric-val">{{ info.deptName }}</div></el-card> <el-card shadow="hover"><div class="metric-title">团队名称</div><div class="metric-val">{{ info.deptName }}</div></el-card>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<el-card shadow="hover"><div class="metric-title">积分余额部门</div><div class="metric-val">{{ info.balance }}</div></el-card> <el-card shadow="hover"><div class="metric-title">团队剩余积分</div><div class="metric-val">{{ info.balance }}</div></el-card>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="16" style="margin-top:16px"> <el-row :gutter="16" style="margin-top:16px">

View File

@ -109,6 +109,9 @@
@close="cancelDeptScore" @close="cancelDeptScore"
> >
<el-form ref="deptScoreFormRef" :model="deptScoreForm" :rules="deptScoreRules" label-width="88px"> <el-form ref="deptScoreFormRef" :model="deptScoreForm" :rules="deptScoreRules" label-width="88px">
<el-form-item label="所属团队">
<span>{{ deptScoreForm.deptName || '—' }}</span>
</el-form-item>
<el-form-item label="用户账号"> <el-form-item label="用户账号">
<span>{{ deptScoreForm.username }}</span> <span>{{ deptScoreForm.username }}</span>
</el-form-item> </el-form-item>
@ -169,6 +172,7 @@ export default {
deptScoreForm: { deptScoreForm: {
userId: null, userId: null,
username: '', username: '',
deptName: '',
amount: undefined, amount: undefined,
remark: '' remark: ''
}, },
@ -270,6 +274,7 @@ export default {
this.deptScoreForm = { this.deptScoreForm = {
userId: row.id, userId: row.id,
username: row.username || row.userId || '', username: row.username || row.userId || '',
deptName: row.deptName || '',
amount: undefined, amount: undefined,
remark: '' remark: ''
} }
@ -306,7 +311,7 @@ export default {
}, },
cancelDeptScore() { cancelDeptScore() {
this.deptScoreOpen = false this.deptScoreOpen = false
this.deptScoreForm = { userId: null, username: '', amount: undefined, remark: '' } this.deptScoreForm = { userId: null, username: '', deptName: '', amount: undefined, remark: '' }
} }
} }
} }

View File

@ -1,10 +1,13 @@
package com.ruoyi.ai.controller; package com.ruoyi.ai.controller;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; import javax.validation.Valid;
import com.ruoyi.ai.domain.request.GroupReportDataRequest; import com.ruoyi.ai.domain.request.GroupReportDataRequest;
import com.ruoyi.ai.domain.vo.TeamDailyConsumeAggregateVO;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -12,10 +15,10 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
@ -44,8 +47,17 @@ public class AiVideoReportDataController extends BaseController {
@PreAuthorize("@ss.hasPermi('ai:data:list')") @PreAuthorize("@ss.hasPermi('ai:data:list')")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(@Valid @ModelAttribute GroupReportDataRequest query) { public TableDataInfo list(@Valid @ModelAttribute GroupReportDataRequest query) {
List<AiVideoReportData> list = aiVideoReportDataService.selectTeamDailyConsumeList(query); TeamDailyConsumeAggregateVO aggregate = aiVideoReportDataService.selectTeamDailyConsumeAggregate(query);
return getDataTable(list); List<AiVideoReportData> list = aiVideoReportDataService.selectTeamDailyConsumeListPaged(query);
TableDataInfo rsp = new TableDataInfo();
rsp.setCode(HttpStatus.SUCCESS);
rsp.setMsg("查询成功");
rsp.setRows(list);
rsp.setTotal(aggregate.getTotalRows());
Map<String, Object> param = new HashMap<>(2);
param.put("aggregate", aggregate);
rsp.setParam(param);
return rsp;
} }
/** /**
@ -54,9 +66,13 @@ public class AiVideoReportDataController extends BaseController {
@PreAuthorize("@ss.hasPermi('ai:data:export')") @PreAuthorize("@ss.hasPermi('ai:data:export')")
@Log(title = "AI视频生成统计数据作为其他统计报的数据源", businessType = BusinessType.EXPORT) @Log(title = "AI视频生成统计数据作为其他统计报的数据源", businessType = BusinessType.EXPORT)
@PostMapping("/export") @PostMapping("/export")
public void export(HttpServletResponse response, @Valid @RequestBody GroupReportDataRequest query) { public void export(HttpServletResponse response, GroupReportDataRequest query) {
if (query != null) {
query.setPageNum(null);
query.setPageSize(null);
}
List<AiVideoReportData> list = aiVideoReportDataService.selectTeamDailyConsumeList(query); List<AiVideoReportData> list = aiVideoReportDataService.selectTeamDailyConsumeList(query);
ExcelUtil<AiVideoReportData> util = new ExcelUtil<AiVideoReportData>(AiVideoReportData.class); ExcelUtil<AiVideoReportData> util = new ExcelUtil<>(AiVideoReportData.class);
util.exportExcel(response, list, "AI视频生成统计数据作为其他统计报的数据源数据"); util.exportExcel(response, list, "AI视频生成统计数据作为其他统计报的数据源数据");
} }

View File

@ -19,8 +19,7 @@ public class SubteamOverviewController extends BaseController {
@PreAuthorize("@ss.hasPermi('subteam:overview:view')") @PreAuthorize("@ss.hasPermi('subteam:overview:view')")
@GetMapping @GetMapping
public AjaxResult overview() { public AjaxResult<SubteamOverviewVO> overview() {
SubteamOverviewVO vo = subteamOverviewService.loadOverview(); return success(subteamOverviewService.loadOverview());
return success(vo);
} }
} }

View File

@ -12,4 +12,6 @@ public final class RedisKey {
* Spring Cache 部门近七日汇总{@code @Cacheable} TTL 配置须同名 * Spring Cache 部门近七日汇总{@code @Cacheable} TTL 配置须同名
*/ */
public static final String CACHE_DEPT_SUMMARY = "dept_summary"; public static final String CACHE_DEPT_SUMMARY = "dept_summary";
public static final String VIDEO_METRICS = "video_metrics";
} }

View File

@ -85,8 +85,8 @@ public class AiUser extends BaseEntity {
private String paymentUrl; private String paymentUrl;
/** 登录时间 */ /** 登录时间 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "登录时间", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date loginTime; private Date loginTime;
/** 余额 */ /** 余额 */

View File

@ -26,43 +26,46 @@ public class AiVideoReportData implements Serializable {
/** 主键 */ /** 主键 */
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
@Excel(name = "主键") @Excel(name = "主键", type = Excel.Type.IMPORT)
private Long id; private Long id;
/** 统计时间,到小时(库表注释:'%Y-%m-%d %H';业务写入常用 yyyyMMddHH */ /** 统计时间,到小时(库表注释:'%Y-%m-%d %H';业务写入常用 yyyyMMddHH */
@Excel(name = "统计时间(到小时)") @Excel(name = "日期", sort = 1)
private String dateKey; private String dateKey;
/** 部门ID */ /** 部门ID */
@Excel(name = "部门ID") @Excel(name = "团队ID", sort = 2)
private Long deptId; private Long deptId;
/** 消耗积分,按任务创建时间统计 */ /** 消耗积分,按任务创建时间统计 */
@Excel(name = "消耗积分") @Excel(name = "消耗积分", sort = 5)
private BigDecimal score; private BigDecimal score;
/** 实际充值积分(充值 - 退款) */ /** 实际充值积分(充值 - 退款) */
@Excel(name = "实际充值积分") @Excel(name = "实际充值积分(充值-退款)", sort = 4)
private BigDecimal rechargeScore; private BigDecimal rechargeScore;
/** 实际订单数,仅统计生成成功的任务 */ /** 实际订单数,仅统计生成成功的任务 */
@Excel(name = "订单数") @Excel(name = "实际订单数量(成功)", sort = 6)
private Long orderCount; private Long orderCount;
/** 三方消耗 tokens按任务创建时间统计 */ /** 三方消耗 tokens按任务创建时间统计 */
@Excel(name = "消耗tokens") @Excel(name = "三方消耗tokens数量", sort = 7)
private Long useTokens; private Long useTokens;
/** 创建时间 */ /** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "创建时间", type = Excel.Type.IMPORT)
private Date createTime; private Date createTime;
/** 更新时间 */ /** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "更新时间", type = Excel.Type.IMPORT)
private Date updateTime; private Date updateTime;
/** 团队名称(查询结果展示,非表字段) */ /** 团队名称(查询结果展示,非表字段) */
@TableField(exist = false) @TableField(exist = false)
@Excel(name = "团队名称", sort = 3)
private String deptName; private String deptName;
/** 查询日期 yyyyMMdd非表字段子团队单日统计等仍可用 */ /** 查询日期 yyyyMMdd非表字段子团队单日统计等仍可用 */

View File

@ -19,4 +19,10 @@ public class GroupReportDataRequest implements Serializable {
/** 二级团队部门 ID可选为空表示不限团队 */ /** 二级团队部门 ID可选为空表示不限团队 */
private Long deptId; private Long deptId;
/** 列表分页:页码,从 1 开始;导出不传 */
private Integer pageNum;
/** 列表分页:每页条数;导出不传 */
private Integer pageSize;
} }

View File

@ -0,0 +1,25 @@
package com.ruoyi.ai.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.Data;
/**
* 团队每日消耗统计分组总行数与明细列全量合计与列表筛选条件一致
*/
@Data
public class TeamDailyConsumeAggregateVO implements Serializable {
private static final long serialVersionUID = 1L;
/** 按日+团队聚合后的行数(与分页 total 一致) */
private long totalRows;
private BigDecimal sumRechargeScore;
private BigDecimal sumScore;
private Long sumOrderCount;
private Long sumUseTokens;
}

View File

@ -1,12 +1,12 @@
package com.ruoyi.ai.mapper; package com.ruoyi.ai.mapper;
import java.util.List; import java.util.List;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ai.domain.AiOrder; import com.ruoyi.ai.domain.AiOrder;
import com.ruoyi.ai.domain.AiVideoReportData; import com.ruoyi.ai.domain.AiVideoReportData;
import com.ruoyi.ai.domain.request.GroupReportDataRequest; import com.ruoyi.ai.domain.request.GroupReportDataRequest;
import com.ruoyi.ai.domain.vo.TeamDailyConsumeAggregateVO;
import com.ruoyi.system.domain.subteam.SubteamVideoMetrics; import com.ruoyi.system.domain.subteam.SubteamVideoMetrics;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@ -24,6 +24,21 @@ public interface AiVideoReportDataMapper extends BaseMapper<AiVideoReportData> {
*/ */
List<AiVideoReportData> selectTeamDailyConsumeList(GroupReportDataRequest query); List<AiVideoReportData> selectTeamDailyConsumeList(GroupReportDataRequest query);
/**
* 与列表相同筛选下的分组行数 + 四列合计一条 SQL
*/
TeamDailyConsumeAggregateVO selectTeamDailyConsumeAggregate(GroupReportDataRequest query);
/**
* 按日+团队聚合后的分页列表一条 SQLLIMIT/OFFSET
* 参数须为标量全局 {@code pagehelper.supportMethodsArguments=true} 若传入含 pageNum/pageSize 的请求对象PageHelper 会再次追加 LIMIT
*/
List<AiVideoReportData> selectTeamDailyConsumeListPaged(@Param("startDate") String startDate,
@Param("endDate") String endDate,
@Param("deptId") Long deptId,
@Param("offset") int offset,
@Param("limit") int limit);
/** /**
* 按团队部门日期聚合团队后台消耗统计 * 按团队部门日期聚合团队后台消耗统计
*/ */

View File

@ -10,6 +10,7 @@ import com.ruoyi.ai.domain.AiVideoReportData;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ai.domain.request.GroupReportDataRequest; import com.ruoyi.ai.domain.request.GroupReportDataRequest;
import com.ruoyi.ai.domain.vo.TeamDailyConsumeAggregateVO;
/** /**
* AI视频生成统计数据作为其他统计报的数据源Service接口 * AI视频生成统计数据作为其他统计报的数据源Service接口
@ -32,6 +33,12 @@ public interface IAiVideoReportDataService {
*/ */
List<AiVideoReportData> selectTeamDailyConsumeList(GroupReportDataRequest query); List<AiVideoReportData> selectTeamDailyConsumeList(GroupReportDataRequest query);
/** 分组总行数 + 四列合计(与列表筛选一致)。 */
TeamDailyConsumeAggregateVO selectTeamDailyConsumeAggregate(GroupReportDataRequest query);
/** 团队每日消耗分页列表(依赖 query 中的 pageNum/pageSizeService 内写入 offset。 */
List<AiVideoReportData> selectTeamDailyConsumeListPaged(GroupReportDataRequest query);
/** /**
* 团队每日消耗按部门 ID团队后台 * 团队每日消耗按部门 ID团队后台
*/ */

View File

@ -1,20 +1,13 @@
package com.ruoyi.ai.service.impl; package com.ruoyi.ai.service.impl;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List; import java.util.List;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.ai.domain.AiChargeRefundOrder; import com.ruoyi.ai.domain.AiChargeRefundOrder;
import com.ruoyi.ai.domain.AiOrder; import com.ruoyi.ai.domain.AiOrder;
import com.ruoyi.ai.domain.request.GroupReportDataRequest; import com.ruoyi.ai.domain.request.GroupReportDataRequest;
import com.ruoyi.common.annotation.Excel; import com.ruoyi.ai.domain.vo.TeamDailyConsumeAggregateVO;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.ai.mapper.AiVideoReportDataMapper; import com.ruoyi.ai.mapper.AiVideoReportDataMapper;
@ -49,6 +42,44 @@ public class AiVideoReportDataServiceImpl implements IAiVideoReportDataService {
return aiVideoReportDataMapper.selectTeamDailyConsumeList(query); return aiVideoReportDataMapper.selectTeamDailyConsumeList(query);
} }
@Override
public TeamDailyConsumeAggregateVO selectTeamDailyConsumeAggregate(GroupReportDataRequest query) {
TeamDailyConsumeAggregateVO vo = aiVideoReportDataMapper.selectTeamDailyConsumeAggregate(query);
if (vo == null) {
vo = new TeamDailyConsumeAggregateVO();
vo.setTotalRows(0L);
vo.setSumRechargeScore(BigDecimal.ZERO);
vo.setSumScore(BigDecimal.ZERO);
vo.setSumOrderCount(0L);
vo.setSumUseTokens(0L);
} else {
if (vo.getSumRechargeScore() == null) {
vo.setSumRechargeScore(BigDecimal.ZERO);
}
if (vo.getSumScore() == null) {
vo.setSumScore(BigDecimal.ZERO);
}
if (vo.getSumOrderCount() == null) {
vo.setSumOrderCount(0L);
}
if (vo.getSumUseTokens() == null) {
vo.setSumUseTokens(0L);
}
}
return vo;
}
@Override
public List<AiVideoReportData> selectTeamDailyConsumeListPaged(GroupReportDataRequest query) {
int pageNum = query.getPageNum() == null || query.getPageNum() < 1 ? 1 : query.getPageNum();
int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 10 : query.getPageSize();
query.setPageNum(pageNum);
query.setPageSize(pageSize);
int offset = (pageNum - 1) * pageSize;
return aiVideoReportDataMapper.selectTeamDailyConsumeListPaged(
query.getStartDate(), query.getEndDate(), query.getDeptId(), offset, pageSize);
}
@Override @Override
public List<AiVideoReportData> selectTeamDailyConsumeByDeptId(String statDate, Long deptId) { public List<AiVideoReportData> selectTeamDailyConsumeByDeptId(String statDate, Long deptId) {
return aiVideoReportDataMapper.selectTeamDailyConsumeByDeptId(statDate, deptId); return aiVideoReportDataMapper.selectTeamDailyConsumeByDeptId(statDate, deptId);

View File

@ -4,6 +4,8 @@ import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.ruoyi.common.constant.RedisKey;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.ai.mapper.AiVideoReportDataMapper; import com.ruoyi.ai.mapper.AiVideoReportDataMapper;
@ -21,6 +23,8 @@ public class SubteamOverviewServiceImpl implements ISubteamOverviewService {
private static final int METRICS_CACHE_MINUTES = 5; private static final int METRICS_CACHE_MINUTES = 5;
private static final DateTimeFormatter VIDEO_METRICS_DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Autowired @Autowired
private ISubteamScopeService subteamScopeService; private ISubteamScopeService subteamScopeService;
@ -48,12 +52,12 @@ public class SubteamOverviewServiceImpl implements ISubteamOverviewService {
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();
LocalDate start = end.minusDays(6); LocalDate start = end.minusDays(6);
String startDay = start.format(DateTimeFormatter.BASIC_ISO_DATE); String startDate = start.format(VIDEO_METRICS_DAY);
String endDay = end.format(DateTimeFormatter.BASIC_ISO_DATE); String endDate = end.format(VIDEO_METRICS_DAY);
String cacheKey = "subteam:videoMetrics:" + deptId + ":" + startDay + ":" + endDay; String cacheKey = RedisKey.VIDEO_METRICS + ":" + deptId + ":" + startDate + ":" + endDate;
SubteamVideoMetrics metrics = redisCache.getCacheObject(cacheKey); SubteamVideoMetrics metrics = redisCache.getCacheObject(cacheKey);
if (metrics == null) { if (metrics == null) {
metrics = aiVideoReportDataMapper.selectDeptVideoMetricsBetween(deptId, startDay, endDay); metrics = aiVideoReportDataMapper.selectDeptVideoMetricsBetween(deptId, startDate, endDate);
if (metrics == null) { if (metrics == null) {
metrics = new SubteamVideoMetrics(); metrics = new SubteamVideoMetrics();
} }

View File

@ -26,7 +26,65 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where id = #{id} where id = #{id}
</select> </select>
<sql id="teamDailyConsumeWhere">
<where>
<if test="startDate != null and startDate != ''">
and substr(vrd.date_key, 1, 10) &gt;= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
and substr(vrd.date_key, 1, 10) &lt;= #{endDate}
</if>
<if test="deptId != null">
and vrd.dept_id = #{deptId}
</if>
</where>
</sql>
<sql id="teamDailyConsumeWhereVrd2">
<where>
<if test="startDate != null and startDate != ''">
and substr(vrd2.date_key, 1, 10) &gt;= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
and substr(vrd2.date_key, 1, 10) &lt;= #{endDate}
</if>
<if test="deptId != null">
and vrd2.dept_id = #{deptId}
</if>
</where>
</sql>
<select id="selectTeamDailyConsumeAggregate" parameterType="com.ruoyi.ai.domain.request.GroupReportDataRequest"
resultType="com.ruoyi.ai.domain.vo.TeamDailyConsumeAggregateVO">
SELECT
(SELECT COUNT(*) FROM (
SELECT 1
FROM ai_video_report_data vrd
LEFT JOIN sys_dept d ON d.dept_id = vrd.dept_id
<include refid="teamDailyConsumeWhere"/>
GROUP BY substr(vrd.date_key, 1, 10), vrd.dept_id, d.dept_name
) cnt) AS total_rows,
IFNULL((SELECT SUM(vrd2.recharge_score) FROM ai_video_report_data vrd2
<include refid="teamDailyConsumeWhereVrd2"/>), 0) AS sum_recharge_score,
IFNULL((SELECT SUM(vrd2.score) FROM ai_video_report_data vrd2
<include refid="teamDailyConsumeWhereVrd2"/>), 0) AS sum_score,
IFNULL((SELECT SUM(vrd2.order_count) FROM ai_video_report_data vrd2
<include refid="teamDailyConsumeWhereVrd2"/>), 0) AS sum_order_count,
IFNULL((SELECT SUM(vrd2.use_tokens) FROM ai_video_report_data vrd2
<include refid="teamDailyConsumeWhereVrd2"/>), 0) AS sum_use_tokens
</select>
<select id="selectTeamDailyConsumeList" parameterType="com.ruoyi.ai.domain.request.GroupReportDataRequest" resultMap="AiVideoReportDataResult"> <select id="selectTeamDailyConsumeList" parameterType="com.ruoyi.ai.domain.request.GroupReportDataRequest" resultMap="AiVideoReportDataResult">
select substr(vrd.date_key, 1, 10) as date_key, vrd.dept_id, d.dept_name, sum(vrd.recharge_score) as recharge_score,
sum(vrd.score) as score, sum(vrd.order_count) as order_count, sum(vrd.use_tokens) as use_tokens
from ai_video_report_data vrd left join sys_dept d on d.dept_id = vrd.dept_id
<include refid="teamDailyConsumeWhere"/>
group by substr(vrd.date_key, 1, 10), vrd.dept_id, d.dept_name
order by substr(vrd.date_key, 1, 10) desc, vrd.dept_id desc
</select>
<select id="selectTeamDailyConsumeListPaged" resultMap="AiVideoReportDataResult">
SELECT * FROM (
select substr(vrd.date_key, 1, 10) as date_key, vrd.dept_id, d.dept_name, sum(vrd.recharge_score) as recharge_score, select substr(vrd.date_key, 1, 10) as date_key, vrd.dept_id, d.dept_name, sum(vrd.recharge_score) as recharge_score,
sum(vrd.score) as score, sum(vrd.order_count) as order_count, sum(vrd.use_tokens) as use_tokens sum(vrd.score) as score, sum(vrd.order_count) as order_count, sum(vrd.use_tokens) as use_tokens
from ai_video_report_data vrd left join sys_dept d on d.dept_id = vrd.dept_id from ai_video_report_data vrd left join sys_dept d on d.dept_id = vrd.dept_id
@ -42,7 +100,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if> </if>
</where> </where>
group by substr(vrd.date_key, 1, 10), vrd.dept_id, d.dept_name group by substr(vrd.date_key, 1, 10), vrd.dept_id, d.dept_name
order by substr(vrd.date_key, 1, 10) desc, vrd.dept_id desc ) x
ORDER BY x.date_key DESC, x.dept_id DESC
LIMIT #{limit} OFFSET #{offset}
</select> </select>
<select id="selectTeamDailyConsumeByDeptId" resultMap="AiVideoReportDataResult"> <select id="selectTeamDailyConsumeByDeptId" resultMap="AiVideoReportDataResult">
@ -62,34 +122,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectDeptVideoMetricsBetween" resultType="com.ruoyi.system.domain.subteam.SubteamVideoMetrics"> <select id="selectDeptVideoMetricsBetween" resultType="com.ruoyi.system.domain.subteam.SubteamVideoMetrics">
select select ifnull(sum(vrd.score), 0) as consumeScore, ifnull(sum(vrd.order_count), 0) as orderCount
coalesce(sum(vrd.score), 0) as consumeScore, from ai_video_report_data vrd where vrd.dept_id = #{deptId}
coalesce(sum(vrd.order_count), 0) as orderCount and substr(vrd.date_key, 1, 10) &gt;= #{startDay}
from ai_video_report_data vrd and substr(vrd.date_key, 1, 10) &lt;= #{endDay}
where vrd.dept_id = #{deptId}
and substr(vrd.date_key, 1, 8) &gt;= #{startDay}
and substr(vrd.date_key, 1, 8) &lt;= #{endDay}
</select> </select>
<insert id="upsertVideoConsumeIncrement">
insert into ai_video_report_data
(date_key, dept_id, score, order_count, use_tokens, recharge_score, create_time, update_time)
values
(#{dateKey}, #{deptId}, #{score}, #{orderCount}, #{useTokens}, 0, now(), now())
on duplicate key update
score = score + values(score),
order_count = order_count + values(order_count),
use_tokens = use_tokens + values(use_tokens),
update_time = now()
</insert>
<insert id="upsertRechargeScoreIncrement">
insert into ai_video_report_data
(date_key, dept_id, score, order_count, use_tokens, recharge_score, create_time, update_time)
values
(#{dateKey}, #{deptId}, 0, 0, 0, 0, #{rechargeScore}, now(), now())
on duplicate key update
recharge_score = recharge_score + values(recharge_score),
update_time = now()
</insert>
</mapper> </mapper>