Merge remote-tracking branch 'origin/seedance_balance' into seedance_balance

This commit is contained in:
old burden 2026-04-20 15:59:40 +08:00
commit ddd2a60f7e
28 changed files with 1441 additions and 59 deletions

View File

@ -44,3 +44,11 @@ export function delDept(deptId) {
method: 'delete' method: 'delete'
}) })
} }
export function chargeRefundDept(data) {
return request({
url: '/ai/dept/charge-refund',
method: 'post',
data: data
})
}

View File

@ -56,18 +56,18 @@ export function changeUserStatus(id, status) {
}) })
} }
export function changeBalance(id, balance) { // 修改余额接口已关闭(与后端 AiUserController.changeBalance 一致停用)
const data = { // export function changeBalance(id, balance) {
id, // const data = {
balance // id,
} // balance
return request({ // }
url: '/ai/user/changeBalance', // return request({
method: 'put', // url: '/ai/user/changeBalance',
data: data // method: 'put',
}) // data: data
} // })
// }
// 用户状态修改 // 用户状态修改
export function updatePassword(id, newPassword) { export function updatePassword(id, newPassword) {
@ -90,3 +90,21 @@ export function assignAiUserDept(data) {
data data
}) })
} }
/** 部门积分下放至用户 */
export function issueDeptScore(data) {
return request({
url: '/ai/user/dept-score/issue',
method: 'put',
data
})
}
/** 用户积分回收至部门 */
export function reclaimDeptScore(data) {
return request({
url: '/ai/user/dept-score/reclaim',
method: 'put',
data
})
}

View File

@ -50,3 +50,21 @@ export function delDept(deptId) {
method: 'delete' method: 'delete'
}) })
} }
// 部门充值/退款
export function chargeRefundDept(data) {
return request({
url: '/system/dept/charge-refund',
method: 'post',
data: data
})
}
// 部门积分更正
export function editScore(data) {
return request({
url: '/system/dept/edit-score',
method: 'post',
data: data
})
}

View File

@ -0,0 +1,76 @@
/**
* 西式千分位en-US金额最多两位小数积分为整数
* 用于输入/粘贴后即时格式化展示
*/
export const WESTERN_MONEY_MAX = 10000000
export const WESTERN_INT_MAX = 100000000
const MONEY_MAX = WESTERN_MONEY_MAX
const INT_MAX = WESTERN_INT_MAX
/** 只保留数字与至多一个小数点,小数位最多 2 位 */
export function sanitizeMoneyDigits(raw) {
let s = String(raw == null ? '' : raw).replace(/,/g, '').replace(/[^\d.]/g, '')
const firstDot = s.indexOf('.')
if (firstDot !== -1) {
s = s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, '').slice(0, 2)
}
return s
}
/** 将规范化后的金额字符串格式化为 9,999,999.99 形式(输入过程中允许末尾为小数点) */
export function formatMoneyWesternDisplay(sanitized) {
if (!sanitized) return ''
const s = sanitized
const dot = s.indexOf('.')
const intRaw = dot === -1 ? s : s.slice(0, dot)
const decRaw = dot === -1 ? '' : s.slice(dot + 1).slice(0, 2)
const intNum = intRaw === '' ? 0 : parseInt(intRaw, 10)
const intFmt = (isNaN(intNum) ? 0 : intNum).toLocaleString('en-US')
if (dot === -1) return intFmt
if (s.endsWith('.') && decRaw === '') return intFmt + '.'
return intFmt + '.' + decRaw
}
/** 规范化字符串 -> 金额数值undefined 表示空) */
export function moneyStringToNumber(sanitized) {
if (sanitized === '' || sanitized === '.' || sanitized == null) return undefined
const n = parseFloat(sanitized)
if (isNaN(n)) return undefined
const rounded = Math.round(n * 100) / 100
if (rounded > MONEY_MAX) return MONEY_MAX
if (rounded < 0) return 0
return rounded
}
/** 失焦时金额固定两位小数展示 */
export function formatMoneyWesternFinal(n) {
if (n === undefined || n === null || isNaN(n)) return ''
const v = Math.min(MONEY_MAX, Math.max(0, Math.round(Number(n) * 100) / 100))
return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
/** 积分:仅数字,并限制范围 */
export function sanitizeIntDigits(raw) {
let d = String(raw == null ? '' : raw).replace(/,/g, '').replace(/\D/g, '')
if (d === '') return ''
let n = parseInt(d, 10)
if (isNaN(n)) return ''
n = Math.min(INT_MAX, Math.max(0, n))
return String(n)
}
export function formatIntWesternDisplay(digits) {
if (!digits) return ''
const n = parseInt(digits, 10)
if (isNaN(n)) return ''
return n.toLocaleString('en-US')
}
export function intStringToNumber(digits) {
if (!digits) return undefined
const n = parseInt(digits, 10)
if (isNaN(n)) return undefined
return n
}

View File

@ -106,8 +106,10 @@
<el-table-column label="主键ID" align="center" prop="id" width="90" /> <el-table-column label="主键ID" align="center" prop="id" width="90" />
<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" />
<!--
<el-table-column label="上级ID" align="center" prop="superiorUuid" /> <el-table-column label="上级ID" align="center" prop="superiorUuid" />
<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" />
@ -135,7 +137,7 @@
</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="source" align="center" prop="source" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="310"> <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
size="mini" size="mini"
@ -151,6 +153,21 @@
@click="updatePassword(scope.row)" @click="updatePassword(scope.row)"
v-hasPermi="['ai:user:edit']" v-hasPermi="['ai:user:edit']"
>修改密码</el-button> >修改密码</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-download"
@click="openDeptScoreDialog(scope.row, 'issue')"
v-hasPermi="['ai:user:deptScoreIssue']"
>下放积分</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-upload2"
@click="openDeptScoreDialog(scope.row, 'reclaim')"
v-hasPermi="['ai:user:deptScoreReclaim']"
>回收积分</el-button>
<!-- 修改余额功能已关闭
<el-button <el-button
size="mini" size="mini"
type="text" type="text"
@ -158,6 +175,7 @@
@click="updateBalance(scope.row)" @click="updateBalance(scope.row)"
v-hasPermi="['ai:user:remove']" v-hasPermi="['ai:user:remove']"
>修改余额</el-button> >修改余额</el-button>
-->
<el-button <el-button
size="mini" size="mini"
type="text" type="text"
@ -263,7 +281,47 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 修改余额对话框 --> <!-- 部门积分下放 / 回收 -->
<el-dialog
:title="deptScoreMode === 'issue' ? '下放积分' : '回收积分'"
:visible.sync="deptScoreOpen"
width="480px"
append-to-body
@close="cancelDeptScore"
>
<el-form ref="deptScoreFormRef" :model="deptScoreForm" :rules="deptScoreRules" label-width="88px">
<el-form-item label="用户账号">
<span>{{ deptScoreForm.username }}</span>
</el-form-item>
<el-form-item label="积分数量" prop="amount">
<el-input-number
v-model="deptScoreForm.amount"
:min="1"
:max="100000000"
:precision="0"
:step="1"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="deptScoreForm.remark"
type="textarea"
:rows="2"
maxlength="50"
show-word-limit
placeholder="选填最多50字"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitDeptScore"> </el-button>
<el-button @click="cancelDeptScore"> </el-button>
</div>
</el-dialog>
<!-- 修改余额功能已关闭
<el-dialog :title="title" :visible.sync="openUpdateBalance" width="500px" append-to-body> <el-dialog :title="title" :visible.sync="openUpdateBalance" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> <el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="余额" prop="balance"> <el-form-item label="余额" prop="balance">
@ -276,6 +334,7 @@
<el-button @click="cancelBalance"> </el-button> <el-button @click="cancelBalance"> </el-button>
</div> </div>
</el-dialog> </el-dialog>
-->
</div> </div>
</template> </template>
@ -286,10 +345,11 @@ import {
delUser, delUser,
addUser, addUser,
updateUser, updateUser,
changeBalance,
changeUserStatus, changeUserStatus,
updatePassword, updatePassword,
assignAiUserDept assignAiUserDept,
issueDeptScore,
reclaimDeptScore
} from "@/api/ai/user"; } from "@/api/ai/user";
import { listDept } from "@/api/ai/dept"; import { listDept } from "@/api/ai/dept";
import Treeselect from "@riophae/vue-treeselect"; import Treeselect from "@riophae/vue-treeselect";
@ -320,7 +380,27 @@ export default {
// //
open: false, open: false,
openUpdatePassword: false, openUpdatePassword: false,
openUpdateBalance: false, deptScoreOpen: false,
deptScoreMode: "issue",
deptScoreForm: {
userId: null,
username: "",
amount: undefined,
remark: ""
},
deptScoreRules: {
amount: [
{ required: true, message: "请输入积分数量", trigger: "blur" },
{
type: "number",
min: 1,
max: 100000000,
message: "请输入 1100000000 的整数",
trigger: "blur"
}
],
remark: [{ max: 50, message: "备注最多50个字", trigger: "blur" }]
},
assignDeptOpen: false, assignDeptOpen: false,
deptOptions: [], deptOptions: [],
assignForm: { assignForm: {
@ -337,7 +417,7 @@ export default {
phone: null, phone: null,
password: null, password: null,
openid: null, openid: null,
status: null, status: "0",
email: null, email: null,
birthday: null, birthday: null,
invitationCode: null, invitationCode: null,
@ -417,9 +497,9 @@ export default {
case "updatePassword": case "updatePassword":
this.updatePassword(row); this.updatePassword(row);
break; break;
case "updateBalance": // case "updateBalance":
this.updateBalance(row); // this.updateBalance(row);
break; // break;
default: default:
break; break;
} }
@ -437,15 +517,62 @@ export default {
}); });
return; return;
}, },
updateBalance(row) { openDeptScoreDialog(row, mode) {
this.reset(); if (!row.deptId) {
const id = row.id || this.ids; this.$modal.msgWarning("请先分配归属部门后再操作积分");
getUser(id).then(response => { return;
this.form = response.data; }
this.openUpdateBalance = true; this.deptScoreMode = mode;
this.title = "修改余额"; this.deptScoreForm = {
userId: row.id,
username: row.username || row.userId || "",
amount: undefined,
remark: ""
};
this.deptScoreOpen = true;
this.$nextTick(() => {
if (this.$refs.deptScoreFormRef) {
this.$refs.deptScoreFormRef.clearValidate();
}
}); });
}, },
submitDeptScore() {
this.$refs.deptScoreFormRef.validate(valid => {
if (!valid) {
return;
}
const raw = this.deptScoreForm.amount;
const amount = typeof raw === "number" ? Math.trunc(raw) : parseInt(String(raw), 10);
if (!Number.isFinite(amount) || amount < 1 || amount > 100000000) {
this.$modal.msgWarning("请输入 1100000000 的整数积分");
return;
}
const payload = {
userId: this.deptScoreForm.userId,
amount,
remark: this.deptScoreForm.remark || undefined
};
const req = this.deptScoreMode === "issue" ? issueDeptScore : reclaimDeptScore;
req(payload).then(() => {
this.$modal.msgSuccess("操作成功");
this.deptScoreOpen = false;
this.getList();
});
});
},
cancelDeptScore() {
this.deptScoreOpen = false;
this.deptScoreForm = { userId: null, username: "", amount: undefined, remark: "" };
},
// updateBalance(row) {
// this.reset();
// const id = row.id || this.ids;
// getUser(id).then(response => {
// this.form = response.data;
// this.openUpdateBalance = true;
// this.title = "";
// });
// },
/** 查询ai-用户信息列表 */ /** 查询ai-用户信息列表 */
getList() { getList() {
this.loading = true; this.loading = true;
@ -475,10 +602,10 @@ export default {
this.open = false; this.open = false;
this.reset(); this.reset();
}, },
cancelBalance() { // cancelBalance() {
this.openUpdateBalance = false; // this.openUpdateBalance = false;
this.reset(); // this.reset();
}, // },
cancelPassword() { cancelPassword() {
this.openUpdatePassword = false; this.openUpdatePassword = false;
this.reset(); this.reset();
@ -567,16 +694,16 @@ export default {
} }
}); });
}, },
handleChangeBalance() { // handleChangeBalance() {
changeBalance; // this.$refs["form"].validate(valid => {
this.$refs["form"].validate(valid => { // if (!valid) return;
changeBalance(this.form.id, this.form.balance).then(response => { // changeBalance(this.form.id, this.form.balance).then(() => {
this.$modal.msgSuccess("修改成功"); // this.$modal.msgSuccess("");
this.openUpdateBalance = false; // this.openUpdateBalance = false;
this.getList(); // this.getList();
}); // });
}); // });
}, // },
/** 删除按钮操作 */ /** 删除按钮操作 */
handleDelete(row) { handleDelete(row) {
const ids = row && row.id != null ? row.id : this.ids; const ids = row && row.id != null ? row.id : this.ids;

View File

@ -57,19 +57,38 @@
:tree-props="{children: 'children', hasChildren: 'hasChildren'}" :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
> >
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column> <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column> <el-table-column prop="orderNum" label="排序" width="80"></el-table-column>
<el-table-column prop="balance" label="剩余积分" width="200" align="right">
<template slot-scope="scope">
<span>{{ scope.row.balance != null ? scope.row.balance : '—' }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100"> <el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/> <dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="200"> <el-table-column label="创建时间" align="center" prop="createTime" width="150">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span> <span>{{ parseTime(scope.row.createTime) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-wallet"
@click="handleChargeRefund(scope.row)"
v-hasPermi="['system:dept:chargeRefund']"
>充值/退款</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-s-operation"
@click="handleEditScore(scope.row)"
v-hasPermi="['system:dept:chargeRefund']"
>积分更正</el-button>
<el-button <el-button
size="mini" size="mini"
type="text" type="text"
@ -215,6 +234,91 @@
<el-button @click="cancel"> </el-button> <el-button @click="cancel"> </el-button>
</div> </div>
</el-dialog> </el-dialog>
<el-dialog
:title="'充值/退款 — ' + (chargeRefundForm.deptName || '')"
:visible.sync="chargeRefundOpen"
width="520px"
append-to-body
@close="resetChargeRefund"
>
<el-form ref="chargeRefundFormRef" :model="chargeRefundForm" :rules="chargeRefundRules" label-width="88px">
<el-form-item label="类型" prop="orderType">
<el-radio-group v-model="chargeRefundForm.orderType">
<el-radio :label="0">充值</el-radio>
<el-radio :label="1">退款</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="金额" prop="money">
<el-input
:value="chargeRefundMoneyDisplay"
placeholder="财务记录(元),如 9,999,999.00"
clearable
@input="onChargeRefundMoneyInput"
@blur="onChargeRefundMoneyBlur"
/>
</el-form-item>
<el-form-item label="积分" prop="amount">
<el-input
:value="chargeRefundAmountDisplay"
placeholder="变动积分,如 99,999,999"
clearable
@input="onChargeRefundAmountInput"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="chargeRefundForm.remark"
type="textarea"
:rows="2"
maxlength="50"
show-word-limit
placeholder="选填最多50字"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitChargeRefund"> </el-button>
<el-button @click="chargeRefundOpen = false"> </el-button>
</div>
</el-dialog>
<el-dialog
:title="'积分更正 — ' + (editScoreForm.deptName || '')"
:visible.sync="editScoreOpen"
width="520px"
append-to-body
@close="resetEditScore"
>
<el-form ref="editScoreFormRef" :model="editScoreForm" :rules="editScoreRules" label-width="88px">
<el-form-item label="积分" prop="score">
<el-input-number
v-model="editScoreForm.score"
:precision="0"
:step="1"
:min="-100000000"
:max="100000000"
controls-position="right"
class="edit-score-input-number"
placeholder="正数增加负数扣减不能为0"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="editScoreForm.remark"
type="textarea"
:rows="2"
maxlength="50"
show-word-limit
placeholder="必填最多50字"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitEditScore"> </el-button>
<el-button @click="editScoreOpen = false"> </el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
@ -241,10 +345,23 @@
color: #909399; color: #909399;
line-height: 1.5; line-height: 1.5;
} }
.edit-score-input-number {
width: 100%;
}
</style> </style>
<script> <script>
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept" import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild, chargeRefundDept, editScore } from "@/api/system/dept"
import {
WESTERN_MONEY_MAX,
sanitizeMoneyDigits,
formatMoneyWesternDisplay,
moneyStringToNumber,
formatMoneyWesternFinal,
sanitizeIntDigits,
formatIntWesternDisplay,
intStringToNumber
} from "@/utils/westernNumberFormat"
import Treeselect from "@riophae/vue-treeselect" import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css" import "@riophae/vue-treeselect/dist/vue-treeselect.css"
@ -278,6 +395,75 @@ export default {
// //
form: {}, form: {},
modelParamRows: [{ label: '', value: '' }], modelParamRows: [{ label: '', value: '' }],
chargeRefundOpen: false,
chargeRefundMoneyDisplay: "",
chargeRefundAmountDisplay: "",
chargeRefundForm: {
deptId: undefined,
deptName: "",
orderType: 0,
money: undefined,
amount: undefined,
remark: ""
},
chargeRefundRules: {
orderType: [
{ required: true, message: "类型不能为空", trigger: "change" }
],
money: [
{ required: true, message: "金额不能为空", trigger: "blur" },
{ type: "number", min: 0, max: 10000000, message: "金额须在 010000000 之间", trigger: "blur" }
],
amount: [
{ required: true, message: "积分不能为空", trigger: "blur" },
{ type: "number", min: 1, max: 100000000, message: "积分须在 1100000000 之间", trigger: "blur" }
],
remark: [
{ max: 50, message: "备注不能超过50个字符", trigger: "blur" }
]
},
editScoreOpen: false,
editScoreForm: {
deptId: undefined,
deptName: "",
score: undefined,
remark: ""
},
editScoreRules: {
score: [
{ required: true, message: "积分不能为空", trigger: "blur" },
{
validator(rule, value, callback) {
if (value === undefined || value === null || value === "") {
callback(new Error("积分不能为空"))
} else if (!Number.isInteger(Number(value))) {
callback(new Error("积分须为整数"))
} else if (Number(value) === 0) {
callback(new Error("积分不能为0"))
} else if (Number(value) < -100000000 || Number(value) > 100000000) {
callback(new Error("积分须在 -100000000100000000 之间不含0"))
} else {
callback()
}
},
trigger: "blur"
}
],
remark: [
{ required: true, message: "备注不能为空", trigger: "blur" },
{
validator(rule, value, callback) {
if (!value || String(value).trim() === "") {
callback(new Error("备注不能为空"))
} else {
callback()
}
},
trigger: "blur"
},
{ max: 50, message: "备注不能超过50个字符", trigger: "blur" }
]
},
// //
rules: { rules: {
parentId: [ parentId: [
@ -467,6 +653,132 @@ export default {
} }
}) })
}, },
resetChargeRefund() {
this.chargeRefundMoneyDisplay = ""
this.chargeRefundAmountDisplay = ""
this.chargeRefundForm = {
deptId: undefined,
deptName: "",
orderType: 0,
money: undefined,
amount: undefined,
remark: ""
}
this.$nextTick(() => {
if (this.$refs.chargeRefundFormRef) {
this.$refs.chargeRefundFormRef.clearValidate()
}
})
},
handleChargeRefund(row) {
this.chargeRefundMoneyDisplay = ""
this.chargeRefundAmountDisplay = ""
this.chargeRefundForm = {
deptId: row.deptId,
deptName: row.deptName,
orderType: 0,
money: undefined,
amount: undefined,
remark: ""
}
this.chargeRefundOpen = true
this.$nextTick(() => {
if (this.$refs.chargeRefundFormRef) {
this.$refs.chargeRefundFormRef.clearValidate()
}
})
},
onChargeRefundMoneyInput(val) {
const sanitized = sanitizeMoneyDigits(val)
if (!sanitized) {
this.chargeRefundMoneyDisplay = ""
this.chargeRefundForm.money = undefined
return
}
const raw = parseFloat(sanitized)
if (!isNaN(raw) && raw > WESTERN_MONEY_MAX) {
this.chargeRefundForm.money = WESTERN_MONEY_MAX
this.chargeRefundMoneyDisplay = formatMoneyWesternFinal(WESTERN_MONEY_MAX)
return
}
this.chargeRefundMoneyDisplay = formatMoneyWesternDisplay(sanitized)
this.chargeRefundForm.money = moneyStringToNumber(sanitized)
},
onChargeRefundMoneyBlur() {
if (this.chargeRefundForm.money !== undefined && this.chargeRefundForm.money !== null) {
this.chargeRefundMoneyDisplay = formatMoneyWesternFinal(this.chargeRefundForm.money)
}
},
onChargeRefundAmountInput(val) {
const digits = sanitizeIntDigits(val)
this.chargeRefundAmountDisplay = digits === "" ? "" : formatIntWesternDisplay(digits)
this.chargeRefundForm.amount = intStringToNumber(digits)
},
submitChargeRefund() {
this.onChargeRefundMoneyBlur()
this.$refs["chargeRefundFormRef"].validate(valid => {
if (!valid) {
return
}
const data = {
deptId: this.chargeRefundForm.deptId,
orderType: this.chargeRefundForm.orderType,
money: this.chargeRefundForm.money,
amount: this.chargeRefundForm.amount,
remark: this.chargeRefundForm.remark ? this.chargeRefundForm.remark.trim() : undefined
}
chargeRefundDept(data).then(() => {
this.$modal.msgSuccess("操作成功")
this.chargeRefundOpen = false
this.getList()
})
})
},
resetEditScore() {
this.editScoreForm = {
deptId: undefined,
deptName: "",
score: undefined,
remark: ""
}
this.$nextTick(() => {
if (this.$refs.editScoreFormRef) {
this.$refs.editScoreFormRef.clearValidate()
}
})
},
handleEditScore(row) {
this.editScoreForm = {
deptId: row.deptId,
deptName: row.deptName,
score: undefined,
remark: ""
}
this.editScoreOpen = true
this.$nextTick(() => {
if (this.$refs.editScoreFormRef) {
this.$refs.editScoreFormRef.clearValidate()
}
})
},
submitEditScore() {
this.$refs["editScoreFormRef"].validate(valid => {
if (!valid) {
return
}
const remark = (this.editScoreForm.remark || "").trim()
const data = {
deptId: this.editScoreForm.deptId,
score: this.editScoreForm.score,
remark: remark
}
editScore(data).then(() => {
this.$modal.msgSuccess("操作成功")
this.editScoreOpen = false
this.getList()
})
})
},
/** 删除按钮操作 */ /** 删除按钮操作 */
handleDelete(row) { handleDelete(row) {
this.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() { this.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {

View File

@ -1,9 +1,8 @@
package com.ruoyi.web.controller.ai; package com.ruoyi.web.controller.ai;
import cn.hutool.core.util.NumberUtil;
import com.ruoyi.ai.service.IAiUserService; import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.ai.service.IDeptUserScoreTransferService;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.BalanceChangerConstants;
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.core.domain.entity.AiUser; import com.ruoyi.common.core.domain.entity.AiUser;
@ -17,11 +16,10 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal; import javax.validation.Valid;
import java.text.SimpleDateFormat;
import java.util.Date; import com.ruoyi.common.core.request.ai.AiUserDeptScoreRequest;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* ai-用户信息Controller * ai-用户信息Controller
@ -36,6 +34,9 @@ public class AiUserController extends BaseController {
@Autowired @Autowired
private IAiUserService aiUserService; private IAiUserService aiUserService;
@Autowired
private IDeptUserScoreTransferService deptUserScoreTransferService;
/** /**
* 查询ai-用户信息列表 * 查询ai-用户信息列表
@ -120,9 +121,10 @@ public class AiUserController extends BaseController {
} }
/** /**
* 状态修改 * 管理端修改余额已关闭如需恢复同步放开 admin-ui api/ai/user.js
*/ */
@ApiOperation("修改ai-用户状态") /*
@ApiOperation("修改ai-用户余额")
@PreAuthorize("@ss.hasPermi('ai:user:edit')") @PreAuthorize("@ss.hasPermi('ai:user:edit')")
@Log(title = "ai-用户信息", businessType = BusinessType.UPDATE) @Log(title = "ai-用户信息", businessType = BusinessType.UPDATE)
@PutMapping("/changeBalance") @PutMapping("/changeBalance")
@ -130,14 +132,15 @@ public class AiUserController extends BaseController {
aiUser.setUpdateBy(getUsername()); aiUser.setUpdateBy(getUsername());
// 获取原余额 // 获取原余额
AiUser u = aiUserService.selectAiUserById(aiUser.getId()); AiUser u = aiUserService.selectAiUserById(aiUser.getId());
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8); String uuid = java.util.UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date()); String dateTime = new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date());
String orderNo = dateTime + uuid; String orderNo = dateTime + uuid;
// 计算变更余额 // 计算变更余额
BigDecimal amount = NumberUtil.sub(aiUser.getBalance(), u.getBalance()); java.math.BigDecimal amount = cn.hutool.core.util.NumberUtil.sub(aiUser.getBalance(), u.getBalance());
aiUserService.addUserBalance(orderNo, aiUser.getId(), amount, BalanceChangerConstants.SYSTEM_OPERATION); aiUserService.addUserBalance(orderNo, aiUser.getId(), amount, com.ruoyi.common.constant.BalanceChangerConstants.SYSTEM_OPERATION);
return toAjax(1); return toAjax(1);
} }
*/
/** /**
* 状态密码 * 状态密码
@ -149,6 +152,30 @@ public class AiUserController extends BaseController {
return toAjax(aiUserService.updatePassword(aiUser)); return toAjax(aiUserService.updatePassword(aiUser));
} }
/**
* 部门积分下放至当前用户余额
*/
@ApiOperation("部门积分下放至用户")
@PreAuthorize("@ss.hasPermi('ai:user:deptScoreIssue')")
@Log(title = "AI用户部门积分下放", businessType = BusinessType.UPDATE)
@PutMapping("/dept-score/issue")
public AjaxResult issueDeptScore(@Valid @RequestBody AiUserDeptScoreRequest request) {
deptUserScoreTransferService.issueDeptScore(request);
return success();
}
/**
* 用户余额回收至部门积分池
*/
@ApiOperation("用户积分回收至部门")
@PreAuthorize("@ss.hasPermi('ai:user:deptScoreReclaim')")
@Log(title = "AI用户部门积分回收", businessType = BusinessType.UPDATE)
@PutMapping("/dept-score/reclaim")
public AjaxResult reclaimDeptScore(@Valid @RequestBody AiUserDeptScoreRequest request) {
deptUserScoreTransferService.reclaimDeptScore(request);
return success();
}
/** /**
* 删除ai-用户信息 * 删除ai-用户信息
*/ */

View File

@ -13,11 +13,14 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; 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.ai.service.IDeptChargeRefundService;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants; import com.ruoyi.common.constant.UserConstants;
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.core.domain.entity.SysDept; import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.request.system.DeptChargeRefundRequest;
import com.ruoyi.common.core.request.system.DeptPointsCorrectionRequest;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService; import com.ruoyi.system.service.ISysDeptService;
@ -34,6 +37,9 @@ public class SysDeptController extends BaseController
@Autowired @Autowired
private ISysDeptService deptService; private ISysDeptService deptService;
@Autowired
private IDeptChargeRefundService deptChargeRefundService;
/** /**
* 获取部门列表 * 获取部门列表
*/ */
@ -129,4 +135,32 @@ public class SysDeptController extends BaseController
deptService.checkDeptDataScope(deptId); deptService.checkDeptDataScope(deptId);
return toAjax(deptService.deleteDeptById(deptId)); return toAjax(deptService.deleteDeptById(deptId));
} }
/**
* 部门手工充值/退款更新部门积分余额并记订单与流水
* 需具备 {@code system:dept:chargeRefund}且目标部门须通过数据权限校验
*/
@PreAuthorize("@ss.hasPermi('system:dept:chargeRefund')")
@Log(title = "部门管理", businessType = BusinessType.UPDATE)
@PostMapping("/charge-refund")
public AjaxResult chargeRefund(@Validated @RequestBody DeptChargeRefundRequest request)
{
deptService.checkDeptDataScope(request.getDeptId());
deptChargeRefundService.chargeOrRefund(request);
return success();
}
/**
* 部门积分更正更新部门积分余额并记流水无订单
* 需具备 {@code system:dept:chargeRefund}且目标部门须通过数据权限校验
*/
@PreAuthorize("@ss.hasPermi('system:dept:chargeRefund')")
@Log(title = "部门管理", businessType = BusinessType.UPDATE)
@PostMapping("/edit-score")
public AjaxResult editScore(@Validated @RequestBody DeptPointsCorrectionRequest request)
{
deptService.checkDeptDataScope(request.getDeptId());
deptService.editScore(request);
return success();
}
} }

View File

@ -73,4 +73,14 @@ public class BalanceChangerConstants {
*/ */
public static final int SYSTEM_OPERATION = 11; public static final int SYSTEM_OPERATION = 11;
/**
* 部门积分下放至用户sys_dept.balance ai_user.balance
*/
public static final int DEPT_SCORE_ISSUE = 12;
/**
* 用户积分回收至部门ai_user.balance sys_dept.balance
*/
public static final int DEPT_SCORE_RECLAIM = 13;
} }

View File

@ -0,0 +1,28 @@
package com.ruoyi.common.core.request.ai;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
/**
* 管理端部门积分与 AI 用户余额互转下放 / 回收
* <p>积分为整数落库仍用 decimal 字段由服务层转为 {@link java.math.BigDecimal}
*/
@Data
public class AiUserDeptScoreRequest {
/** 对应 ai_user.id主键 */
@NotNull(message = "用户不能为空")
private Long userId;
/** 积分数量(正整数) */
@NotNull(message = "积分不能为空")
@Min(value = 1, message = "积分必须为正整数")
@Max(value = 100_000_000, message = "积分超出上限")
private Long amount;
@Size(max = 50, message = "备注最多50个字")
private String remark;
}

View File

@ -0,0 +1,47 @@
package com.ruoyi.common.core.request.system;
import java.math.BigDecimal;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
/**
* 部门手工充值/退款请求后台入账
* <p>
* 业务约定{@code amount}积分驱动 {@code sys_dept.balance} 增减{@code money} 仅写入订单表作财务记录不参与余额计算
* 校验规则需与前端充值弹窗一致
*/
@Data
public class DeptChargeRefundRequest {
/** 目标部门 */
@NotNull(message = "部门不能为空")
private Long deptId;
/** 0-充值 1-退款,对应 {@link com.ruoyi.common.enums.ChargeRefundOrderType} */
@NotNull(message = "订单类型不能为空")
@Min(value = 0, message = "订单类型不合法")
@Max(value = 1, message = "订单类型不合法")
private Integer orderType;
/** 财务记录金额(两位小数),不参与余额运算;必填,允许为 0 */
@NotNull(message = "金额不能为空")
@DecimalMin(value = "0", inclusive = true, message = "金额不能小于0")
@DecimalMax(value = "10000000", message = "金额不能超过10000000")
@Digits(integer = 8, fraction = 2, message = "金额最多两位小数")
private BigDecimal money;
/** 积分变动量(正整数),与 sys_dept.balance 增减一致 */
@NotNull(message = "积分不能为空")
@Min(value = 1, message = "积分不能小于1")
@Max(value = 100_000_000, message = "积分不能超过100000000")
private Long amount;
@Size(max = 50, message = "备注不能超过50个字符")
private String remark;
}

View File

@ -0,0 +1,31 @@
package com.ruoyi.common.core.request.system;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
/**
* 部门积分更正请求仅更新余额与流水不产生充值/退款订单
*/
@Data
public class DeptPointsCorrectionRequest {
/** 目标部门 */
@NotNull(message = "部门不能为空")
private Long deptId;
/**
* 积分变动量可正可负不可为 0 {@code sys_dept.balance} 增减一致
*/
@NotNull(message = "积分不能为空")
@Min(value = -100_000_000, message = "积分不能小于-100000000")
@Max(value = 100_000_000, message = "积分不能超过100000000")
private Long score;
@NotBlank(message = "备注不能为空")
@Size(max = 50, message = "备注不能超过50个字符")
private String remark;
}

View File

@ -11,7 +11,7 @@ public enum GroupBalanceChangeType {
RECHARGE(0, "充值"), RECHARGE(0, "充值"),
REFUND(1, "退款"), REFUND(1, "退款"),
ISSUE(2, "下发"), ISSUE(2, "下发"),
CONSUME(3, "消费"), RECLAIM(3, "回收"),
MANUAL_ADJUST(4, "手动修改"); MANUAL_ADJUST(4, "手动修改");
private final int code; private final int code;

View File

@ -29,6 +29,13 @@
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- 与 ruoyi-framework 对齐,供部门-用户积分下放/回收 Redisson 锁使用 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
<!-- <dependency>--> <!-- <dependency>-->
<!-- <groupId>com.byteplus</groupId>--> <!-- <groupId>com.byteplus</groupId>-->

View File

@ -51,7 +51,7 @@ public class AiBalanceChangeRecord extends BaseEntity {
private String uuid; private String uuid;
/** /**
* 操作类型0-充值 1-返佣 2-充值赠送 3-体验金赠送 4-体验金回收 5-图生图 6-一键换脸 7-快捷生图 8-快捷生视频 9-退款 10-系统操作 * 操作类型0-充值 1-返佣 2-充值赠送 3-体验金赠送 4-体验金回收 5-图生图 6-一键换脸 7-快捷生图 8-快捷生视频 9-退款 10-系统操作 12-部门下放积分 13-回收至部门
*/ */
@Excel(name = "操作类型") @Excel(name = "操作类型")
private Integer type; private Integer type;

View File

@ -30,4 +30,9 @@ public interface AiOrderMapper extends BaseMapper<AiOrder> {
@Select("SELECT * FROM ai_order WHERE third_party_order_num = #{id} LIMIT 1") @Select("SELECT * FROM ai_order WHERE third_party_order_num = #{id} LIMIT 1")
AiOrder selectOneByThirdPartyOrderNum(@Param("id") String id); AiOrder selectOneByThirdPartyOrderNum(@Param("id") String id);
/**
* 未完结订单数量del_flag=0 status 非已完成(1)非失败(2)即进行中(0)
*/
int countOpenOrdersByUserId(@Param("userId") Long userId);
} }

View File

@ -0,0 +1,16 @@
package com.ruoyi.ai.service;
import com.ruoyi.common.core.request.system.DeptChargeRefundRequest;
/**
* 部门手工充值/退款同一事务内写入订单更新部门积分写入集团余额流水
*/
public interface IDeptChargeRefundService {
/**
* 执行充值或退款任一步失败则整笔回滚含已插入的订单行
*
* @param request 已通过 Bean Validation 的入参
*/
void chargeOrRefund(DeptChargeRefundRequest request);
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.ai.service;
import com.ruoyi.common.core.request.ai.AiUserDeptScoreRequest;
/**
* 部门积分与 AI 用户余额互转下放 / 回收 Redisson
*/
public interface IDeptUserScoreTransferService {
void issueDeptScore(AiUserDeptScoreRequest request);
void reclaimDeptScore(AiUserDeptScoreRequest request);
}

View File

@ -0,0 +1,122 @@
package com.ruoyi.ai.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.ai.domain.AiChargeRefundOrder;
import com.ruoyi.ai.domain.AiGroupBalanceChangeRecord;
import com.ruoyi.ai.service.IAiChargeRefundOrderService;
import com.ruoyi.ai.service.IAiGroupBalanceChangeRecordService;
import com.ruoyi.ai.service.IDeptChargeRefundService;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.request.system.DeptChargeRefundRequest;
import com.ruoyi.common.enums.ChargeRefundOrderStatusType;
import com.ruoyi.common.enums.ChargeRefundOrderType;
import com.ruoyi.common.enums.GroupBalanceChangeType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.service.ISysDeptService;
/**
* 部门手工充值/退款实现
* <p>
* 顺序生成单号 {@code ai_charge_refund_order}同步成功态 原子更新 {@code sys_dept.balance}
* 查最新余额 {@code ai_group_balance_change_record}带符号变动量变动后余额
*/
@Service
public class DeptChargeRefundServiceImpl implements IDeptChargeRefundService {
@Autowired
private ISysDeptService deptService;
@Autowired
private IAiChargeRefundOrderService aiChargeRefundOrderService;
@Autowired
private IAiGroupBalanceChangeRecordService aiGroupBalanceChangeRecordService;
@Override
@Transactional(rollbackFor = Exception.class)
public void chargeOrRefund(DeptChargeRefundRequest request) {
ChargeRefundOrderType orderType = ChargeRefundOrderType.fromCode(request.getOrderType());
if (orderType == null) {
throw new ServiceException("订单类型不合法");
}
// 积分变动量 sys_dept.balance 增减一致money 仅落库订单不参与此处运算
BigDecimal amountBd = BigDecimal.valueOf(request.getAmount());
String orderNum = buildOrderNum();
Date now = DateUtils.getNowDate();
// 1) 手工入账订单状态直接为已完成
AiChargeRefundOrder order = new AiChargeRefundOrder();
order.setDelFlag("0");
order.setCreateBy(SecurityUtils.getUserId());
order.setCreateTime(now);
order.setUpdateTime(now);
order.setOrderNum(orderNum);
order.setDeptId(request.getDeptId());
order.setOrderType(request.getOrderType());
// 金额两位小数向零截断非四舍五入避免入账金额被抬高
order.setMoney(request.getMoney().setScale(2, RoundingMode.DOWN));
order.setAmount(amountBd);
order.setRemark(request.getRemark());
order.setStatus(ChargeRefundOrderStatusType.SUCCESS.getCode());
aiChargeRefundOrderService.insert(order);
// 2) 原子更新部门积分退款时 rows==0 表示余额不足或部门无效依赖事务回滚撤销上一 INSERT
int rows;
if (orderType == ChargeRefundOrderType.CHARGE) {
rows = deptService.addDeptBalance(request.getDeptId(), amountBd);
} else {
rows = deptService.subtractDeptBalance(request.getDeptId(), amountBd);
}
if (rows == 0) {
if (orderType == ChargeRefundOrderType.REFUND) {
throw new ServiceException("余额不足或部门不存在");
}
throw new ServiceException("部门不存在或已删除");
}
SysDept dept = deptService.selectDeptById(request.getDeptId());
if (dept == null) {
throw new ServiceException("部门不存在");
}
// 3) 流水充值为正退款为负result_amount 为变动后部门余额
BigDecimal signedChange = orderType == ChargeRefundOrderType.CHARGE
? amountBd
: amountBd.negate();
AiGroupBalanceChangeRecord record = new AiGroupBalanceChangeRecord();
record.setRelationOrderNo(orderNum);
record.setDeptId(request.getDeptId());
record.setType(orderType == ChargeRefundOrderType.CHARGE
? GroupBalanceChangeType.RECHARGE.getCode()
: GroupBalanceChangeType.REFUND.getCode());
record.setChangeAmount(signedChange);
record.setResultAmount(dept.getBalance());
record.setRemark(request.getRemark());
record.setCreateBy(SecurityUtils.getUserId());
record.setCreateTime(now);
record.setUpdateTime(now);
aiGroupBalanceChangeRecordService.insert(record);
}
/** 业务单号前缀 + 日序 + 随机后缀,保证与流水 relation_order_no 一致。 */
private static String buildOrderNum() {
String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
return "CG" + dateTime + uuid;
}
}

View File

@ -0,0 +1,135 @@
package com.ruoyi.ai.service.impl;
import java.util.concurrent.TimeUnit;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.ai.service.IDeptUserScoreTransferService;
import com.ruoyi.common.core.domain.entity.AiUser;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.request.ai.AiUserDeptScoreRequest;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService;
/**
* 部门与用户积分互转先占 Redisson 再进入事务内层
*/
@Service
public class DeptUserScoreTransferServiceImpl implements IDeptUserScoreTransferService {
private static final String USER_LOCK_PREFIX = "ai:user-score:";
private static final int LOCK_WAIT_SECONDS = 10;
private static final int LOCK_LEASE_SECONDS = 10;
@Autowired
private RedissonClient redissonClient;
@Autowired
private IAiUserService aiUserService;
@Autowired
private DeptUserScoreTransferTxService deptUserScoreTransferTxService;
@Autowired
private ISysDeptService deptService;
@Override
public void issueDeptScore(AiUserDeptScoreRequest request) {
AiUser user = loadUserForDeptOp(request.getUserId());
assertOperatorDeptRelatedToTarget(user.getDeptId());
runWithLock(user.getId(), () -> deptUserScoreTransferTxService.transferIssue(request));
}
@Override
public void reclaimDeptScore(AiUserDeptScoreRequest request) {
AiUser user = loadUserForDeptOp(request.getUserId());
assertOperatorDeptRelatedToTarget(user.getDeptId());
runWithLock(user.getId(), () -> deptUserScoreTransferTxService.transferReclaim(request));
}
private AiUser loadUserForDeptOp(Long id) {
AiUser user = aiUserService.selectAiUserById(id);
if (user == null) {
throw new ServiceException("用户不存在");
}
if (user.getDeptId() == null) {
throw new ServiceException("用户未分配部门,无法操作");
}
if (user.getDelFlag() != null && !"0".equals(user.getDelFlag())) {
throw new ServiceException("用户已删除或无效");
}
return user;
}
/**
* 当前登录用户部门与目标用户部门须相同或一方在另一方祖级链上上下级
* 超级管理员不校验部门
*/
private void assertOperatorDeptRelatedToTarget(Long targetUserDeptId) {
if (SecurityUtils.isAdmin(SecurityUtils.getUserId())) {
return;
}
Long operatorDeptId = SecurityUtils.getDeptId();
if (operatorDeptId == null) {
throw new ServiceException("当前账号无归属部门,无法操作");
}
if (targetUserDeptId == null) {
throw new ServiceException("用户未分配部门,无法操作");
}
if (operatorDeptId.equals(targetUserDeptId)) {
return;
}
SysDept targetDept = deptService.selectDeptById(targetUserDeptId);
SysDept operatorDept = deptService.selectDeptById(operatorDeptId);
if (targetDept == null || operatorDept == null) {
throw new ServiceException("部门数据异常");
}
// 目标部门在操作者部门之下或操作者部门在目标部门之下
if (ancestorsChainContainsDeptId(targetDept.getAncestors(), operatorDeptId)
|| ancestorsChainContainsDeptId(operatorDept.getAncestors(), targetUserDeptId)) {
return;
}
throw new ServiceException("仅可操作与本部门相同或存在上下级关系的部门用户");
}
/**
* 判断 {@code ancestors}逗号分隔祖级 id中是否包含 {@code deptId}含与自身部门 id 一致的场景由调用方先处理
*/
private static boolean ancestorsChainContainsDeptId(String ancestors, Long deptId) {
if (deptId == null || StringUtils.isEmpty(ancestors)) {
return false;
}
String needle = String.valueOf(deptId);
for (String segment : ancestors.split(",")) {
if (needle.equals(segment.trim())) {
return true;
}
}
return false;
}
private void runWithLock(Long userId, Runnable inLock) {
String key = USER_LOCK_PREFIX + userId;
RLock lock = redissonClient.getLock(key);
boolean locked = false;
try {
locked = lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS);
if (!locked) {
throw new ServiceException("系统繁忙,请稍后再试");
}
inLock.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("操作被中断");
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}

View File

@ -0,0 +1,149 @@
package com.ruoyi.ai.service.impl;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.ai.domain.AiGroupBalanceChangeRecord;
import com.ruoyi.ai.mapper.AiOrderMapper;
import com.ruoyi.ai.service.IAiGroupBalanceChangeRecordService;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.core.domain.entity.AiUser;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.request.ai.AiUserDeptScoreRequest;
import com.ruoyi.common.enums.GroupBalanceChangeType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService;
/**
* 部门与用户积分互转的事务内逻辑由门面在 Redisson 锁内调用
*/
@Service
public class DeptUserScoreTransferTxService {
@Autowired
private ISysDeptService deptService;
@Autowired
private IAiUserService aiUserService;
@Autowired
private IAiGroupBalanceChangeRecordService aiGroupBalanceChangeRecordService;
@Autowired
private AiOrderMapper aiOrderMapper;
@Transactional(rollbackFor = Exception.class)
public void transferIssue(AiUserDeptScoreRequest request) {
AiUser user = requireUserWithDept(request.getUserId());
Long deptId = user.getDeptId();
BigDecimal amount = toAmountBigDecimal(request.getAmount());
String orderNum = buildOrderNum();
String remark = buildRemark(request.getRemark(), "部门下放积分至用户");
int rows = deptService.subtractDeptBalance(deptId, amount);
if (rows == 0) {
throw new ServiceException("部门积分不足或部门不存在");
}
aiUserService.addUserBalance(orderNum, user.getId(), amount, BalanceChangerConstants.DEPT_SCORE_ISSUE, remark);
BigDecimal deptBalAfter = getDeptBalance(deptId);
insertGroupRecord(orderNum, deptId, GroupBalanceChangeType.ISSUE.getCode(), amount.negate(), deptBalAfter, remark);
}
@Transactional(rollbackFor = Exception.class)
public void transferReclaim(AiUserDeptScoreRequest request) {
AiUser user = requireUserWithDept(request.getUserId());
Long deptId = user.getDeptId();
BigDecimal amount = toAmountBigDecimal(request.getAmount());
int openCnt = aiOrderMapper.countOpenOrdersByUserId(user.getId());
if (openCnt > 0) {
throw new ServiceException("存在未完结订单,无法回收积分");
}
BigDecimal userBal = user.getBalance() == null ? BigDecimal.ZERO : user.getBalance();
if (userBal.compareTo(amount) < 0) {
throw new ServiceException("用户积分不足");
}
String orderNum = buildOrderNum();
String remark = buildRemark(request.getRemark(), "用户积分回收至部门");
aiUserService.addUserBalance(orderNum, user.getId(), amount.negate(), BalanceChangerConstants.DEPT_SCORE_RECLAIM, remark);
int rows = deptService.addDeptBalance(deptId, amount);
if (rows == 0) {
throw new ServiceException("部门不存在或已删除");
}
BigDecimal deptBalAfter = getDeptBalance(deptId);
insertGroupRecord(orderNum, deptId, GroupBalanceChangeType.RECLAIM.getCode(), amount, deptBalAfter, remark);
}
private AiUser requireUserWithDept(Long id) {
AiUser user = aiUserService.selectAiUserById(id);
if (user == null) {
throw new ServiceException("用户不存在");
}
if (user.getDeptId() == null) {
throw new ServiceException("用户未分配部门,无法操作");
}
if (user.getDelFlag() != null && !"0".equals(user.getDelFlag())) {
throw new ServiceException("用户已删除或无效");
}
return user;
}
/** 请求为整数积分,与库表 decimal 对接(无小数) */
private static BigDecimal toAmountBigDecimal(Long amount) {
if (amount == null || amount < 1) {
throw new ServiceException("积分必须为正整数");
}
return BigDecimal.valueOf(amount.longValue());
}
private String buildRemark(String input, String defaultPrefix) {
if (StringUtils.isNotEmpty(input)) {
return input;
}
return defaultPrefix;
}
private BigDecimal getDeptBalance(Long deptId) {
SysDept dept = deptService.selectDeptById(deptId);
if (dept == null) {
throw new ServiceException("部门不存在");
}
return dept.getBalance() == null ? BigDecimal.ZERO : dept.getBalance();
}
private void insertGroupRecord(String orderNum, Long deptId, int type, BigDecimal signedChange,
BigDecimal resultBalance, String remark) {
AiGroupBalanceChangeRecord record = new AiGroupBalanceChangeRecord();
record.setRelationOrderNo(orderNum);
record.setDeptId(deptId);
record.setType(type);
record.setChangeAmount(signedChange);
record.setResultAmount(resultBalance);
record.setRemark(remark);
record.setCreateBy(SecurityUtils.getUserId());
aiGroupBalanceChangeRecordService.insert(record);
}
private static String buildOrderNum() {
String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
return "DU" + dateTime + uuid;
}
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.system.mapper; package com.ruoyi.system.mapper;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.common.core.domain.entity.SysDept;
@ -117,4 +118,21 @@ public interface SysDeptMapper
public int deleteDeptById(Long deptId); public int deleteDeptById(Long deptId);
public int getMaxOrder(Long deptId); public int getMaxOrder(Long deptId);
/**
* 原子增加部门积分余额充值{@code balance += amount} {@code del_flag = '0'} 的部门
* 用于并发下避免先读后写 {@link #subtractDeptBalance} 成对使用
*
* @param amount 积分变动量正数与订单 {@code ai_charge_refund_order.amount} 一致
* @return 影响行数0 表示部门不存在或已删除
*/
int addDeptBalance(@Param("deptId") Long deptId, @Param("amount") BigDecimal amount);
/**
* 原子扣减部门积分余额退款条件 {@code IFNULL(balance,0) >= amount} 时执行 {@code balance -= amount}防止透支
*
* @param amount 积分变动量正数
* @return 影响行数0 表示余额不足部门不存在或已删除
*/
int subtractDeptBalance(@Param("deptId") Long deptId, @Param("amount") BigDecimal amount);
} }

View File

@ -1,8 +1,10 @@
package com.ruoyi.system.service; package com.ruoyi.system.service;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import com.ruoyi.common.core.domain.TreeSelect; import com.ruoyi.common.core.domain.TreeSelect;
import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.request.system.DeptPointsCorrectionRequest;
/** /**
* 部门管理 服务层 * 部门管理 服务层
@ -123,4 +125,23 @@ public interface ISysDeptService
public int deleteDeptById(Long deptId); public int deleteDeptById(Long deptId);
public int getMaxOrder(Long deptId); public int getMaxOrder(Long deptId);
/**
* 原子增加部门积分余额充值 {@link com.ruoyi.system.mapper.SysDeptMapper#addDeptBalance}
*
* @param amount 积分增量与手工入账订单中的积分字段一致
*/
int addDeptBalance(Long deptId, BigDecimal amount);
/**
* 原子扣减部门积分余额退款 {@link com.ruoyi.system.mapper.SysDeptMapper#subtractDeptBalance}
*
* @param amount 积分减量正数
*/
int subtractDeptBalance(Long deptId, BigDecimal amount);
/**
* 积分更正更新部门余额并写入集团流水手动修改类型不产生充值/退款订单
*/
void editScore(DeptPointsCorrectionRequest request);
} }

View File

@ -1,21 +1,29 @@
package com.ruoyi.system.service.impl; package com.ruoyi.system.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.ruoyi.ai.domain.AiGroupBalanceChangeRecord;
import com.ruoyi.ai.service.IAiGroupBalanceChangeRecordService;
import com.ruoyi.common.EncryptionService; import com.ruoyi.common.EncryptionService;
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 org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.annotation.DataScope; import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.constant.UserConstants; import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.TreeSelect; import com.ruoyi.common.core.domain.TreeSelect;
import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysRole; import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.request.system.DeptPointsCorrectionRequest;
import com.ruoyi.common.core.text.Convert; import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.enums.GroupBalanceChangeType;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.common.utils.spring.SpringUtils;
@ -41,6 +49,9 @@ public class SysDeptServiceImpl implements ISysDeptService
@Resource @Resource
private EncryptionService encryptionService; private EncryptionService encryptionService;
@Autowired
private IAiGroupBalanceChangeRecordService aiGroupBalanceChangeRecordService;
/** /**
* 查询部门管理数据 * 查询部门管理数据
* *
@ -324,6 +335,68 @@ public class SysDeptServiceImpl implements ISysDeptService
return maxOrder != null ? maxOrder : 1; return maxOrder != null ? maxOrder : 1;
} }
/** 委托 Mapper 原子更新 {@code sys_dept.balance}(充值)。 */
@Override
public int addDeptBalance(Long deptId, BigDecimal amount)
{
return deptMapper.addDeptBalance(deptId, amount);
}
/** 委托 Mapper 原子更新 {@code sys_dept.balance}(退款,防透支)。 */
@Override
public int subtractDeptBalance(Long deptId, BigDecimal amount)
{
return deptMapper.subtractDeptBalance(deptId, amount);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void editScore(DeptPointsCorrectionRequest request)
{
Long score = request.getScore();
if (score == null || score == 0L)
{
throw new ServiceException("积分不能为0");
}
SysDept dept = selectDeptById(request.getDeptId());
if (dept == null)
{
throw new ServiceException("部门不存在");
}
BigDecimal delta = BigDecimal.valueOf(score);
int rows;
if (delta.signum() > 0)
{
rows = addDeptBalance(request.getDeptId(), delta);
}
else
{
rows = subtractDeptBalance(request.getDeptId(), delta.abs());
}
if (rows == 0)
{
if (delta.signum() < 0)
{
throw new ServiceException("余额不足或部门不存在");
}
throw new ServiceException("部门不存在或已删除");
}
AiGroupBalanceChangeRecord record = new AiGroupBalanceChangeRecord();
record.setRelationOrderNo(null);
record.setDeptId(request.getDeptId());
record.setType(GroupBalanceChangeType.MANUAL_ADJUST.getCode());
record.setChangeAmount(delta);
record.setResultAmount(dept.getBalance());
record.setRemark(request.getRemark());
record.setCreateBy(SecurityUtils.getUserId());
aiGroupBalanceChangeRecordService.insert(record);
}
/** /**
* 递归列表 * 递归列表
*/ */

View File

@ -173,4 +173,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<select id="countOpenOrdersByUserId" resultType="int">
select count(1) from ai_order
where user_id = #{userId}
and del_flag = '0'
and status=0
</select>
</mapper> </mapper>

View File

@ -180,4 +180,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
update sys_dept set del_flag = '2' where dept_id = #{deptId} update sys_dept set del_flag = '2' where dept_id = #{deptId}
</delete> </delete>
<!-- 部门积分充值:单行原子加余额 -->
<update id="addDeptBalance">
update sys_dept
set balance = IFNULL(balance, 0) + #{amount},
update_time = sysdate()
where dept_id = #{deptId} and del_flag = '0'
</update>
<!-- 部门积分退款:条件更新,余额不足则影响行数为 0调用方需回滚事务并提示 -->
<update id="subtractDeptBalance">
update sys_dept
set balance = IFNULL(balance, 0) - #{amount},
update_time = sysdate()
where dept_id = #{deptId} and del_flag = '0' and IFNULL(balance, 0) &gt;= #{amount}
</update>
</mapper> </mapper>

View File

@ -0,0 +1,61 @@
-- =============================================================================
-- AI用户管理 下级权限:下放积分、回收积分(菜单类型 F挂在「AI用户管理」下
-- 仅拥有对应 perms 的角色在页面可看到按钮v-hasPermi
-- 可重复执行:已存在相同 perms 时跳过插入
-- =============================================================================
-- 父菜单AI用户管理C 菜单component 一般为 ai/user/index
SET @ai_user_menu_id := (
SELECT menu_id FROM sys_menu
WHERE menu_type = 'C'
AND (perms = 'ai:user:list' OR perms LIKE 'ai:user:list')
AND (path = 'aiUser' OR component = 'ai/user/index' OR component LIKE '%ai/user/index%')
ORDER BY menu_id
LIMIT 1
);
-- 若未找到父菜单,后续 INSERT 会失败,请先核对 sys_menu 中 AI 用户页配置
INSERT INTO sys_menu (
menu_name, parent_id, order_num, path, component, `query`, route_name,
is_frame, is_cache, menu_type, visible, status, perms, icon,
create_by, create_time, remark
)
SELECT
'下放积分', @ai_user_menu_id, 1, '', '', NULL, '',
1, 0, 'F', '0', '0', 'ai:user:deptScoreIssue', '#',
'admin', NOW(), '部门积分下放至用户sys_dept.balance → ai_user.balance'
FROM (SELECT 1) AS _x
WHERE @ai_user_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'ai:user:deptScoreIssue');
INSERT INTO sys_menu (
menu_name, parent_id, order_num, path, component, `query`, route_name,
is_frame, is_cache, menu_type, visible, status, perms, icon,
create_by, create_time, remark
)
SELECT
'回收积分', @ai_user_menu_id, 2, '', '', NULL, '',
1, 0, 'F', '0', '0', 'ai:user:deptScoreReclaim', '#',
'admin', NOW(), '用户积分回收至部门(需无未完结订单)'
FROM (SELECT 1) AS _y
WHERE @ai_user_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'ai:user:deptScoreReclaim');
-- 已勾选「AI用户管理」菜单的角色自动勾选上述两个按钮避免角色树里看不到或未授权
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT rm.role_id, m.menu_id
FROM sys_role_menu rm
JOIN sys_menu m ON m.perms = 'ai:user:deptScoreIssue'
WHERE rm.menu_id = @ai_user_menu_id
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id
);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT rm.role_id, m.menu_id
FROM sys_role_menu rm
JOIN sys_menu m ON m.perms = 'ai:user:deptScoreReclaim'
WHERE rm.menu_id = @ai_user_menu_id
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id
);

View File

@ -4676,6 +4676,9 @@ INSERT INTO `sys_menu` VALUES (2001, 'AI用户管理', 2003, 1, 'aiUser', 'ai/us
INSERT INTO `sys_menu` VALUES (2002, 'AI管理', 2000, 4, 'aiManager', 'ai/manager/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:manager:list', 'online', 'admin', '2025-11-13 19:18:54', 'admin', '2025-11-13 22:49:28', ''); INSERT INTO `sys_menu` VALUES (2002, 'AI管理', 2000, 4, 'aiManager', 'ai/manager/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:manager:list', 'online', 'admin', '2025-11-13 19:18:54', 'admin', '2025-11-13 22:49:28', '');
INSERT INTO `sys_menu` VALUES (2003, '用户管理', 2000, 1, 'aiUserManger', NULL, NULL, '', 1, 0, 'M', '0', '0', NULL, 'peoples', 'admin', '2025-11-13 22:18:42', '', NULL, ''); INSERT INTO `sys_menu` VALUES (2003, '用户管理', 2000, 1, 'aiUserManger', NULL, NULL, '', 1, 0, 'M', '0', '0', NULL, 'peoples', 'admin', '2025-11-13 22:18:42', '', NULL, '');
INSERT INTO `sys_menu` VALUES (2004, '余额使用记录', 2003, 2, 'balanceChangeRecord', 'ai/balanceChangeRecord/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:balanceChangeRecord:list', 'nested', 'admin', '2025-11-13 22:31:42', '', NULL, ''); INSERT INTO `sys_menu` VALUES (2004, '余额使用记录', 2003, 2, 'balanceChangeRecord', 'ai/balanceChangeRecord/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:balanceChangeRecord:list', 'nested', 'admin', '2025-11-13 22:31:42', '', NULL, '');
-- AI用户管理(2001)下级按钮:下放/回收积分(权限 ai:user:deptScoreIssue / ai:user:deptScoreReclaim已存在时可跳过或执行 sql/ai_user_dept_score_menu.sql
INSERT INTO `sys_menu` VALUES (2070, '下放积分', 2001, 3, '', '', NULL, '', 1, 0, 'F', '0', '0', 'ai:user:deptScoreIssue', '#', 'admin', '2025-11-13 22:31:42', '', NULL, '部门积分下放至用户');
INSERT INTO `sys_menu` VALUES (2071, '回收积分', 2001, 4, '', '', NULL, '', 1, 0, 'F', '0', '0', 'ai:user:deptScoreReclaim', '#', 'admin', '2025-11-13 22:31:42', '', NULL, '用户积分回收至部门');
INSERT INTO `sys_menu` VALUES (2005, '返佣记录', 2003, 2, 'rebateRecord', 'ai/rebateRecord/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:rebateRecord:list', 'monitor', 'admin', '2025-11-13 22:32:39', '', NULL, ''); INSERT INTO `sys_menu` VALUES (2005, '返佣记录', 2003, 2, 'rebateRecord', 'ai/rebateRecord/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:rebateRecord:list', 'monitor', 'admin', '2025-11-13 22:32:39', '', NULL, '');
INSERT INTO `sys_menu` VALUES (2006, '订单记录', 2000, 5, 'order', 'ai/order/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:order:list', 'server', 'admin', '2025-11-13 22:36:36', 'admin', '2025-11-29 16:12:14', ''); INSERT INTO `sys_menu` VALUES (2006, '订单记录', 2000, 5, 'order', 'ai/order/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:order:list', 'server', 'admin', '2025-11-13 22:36:36', 'admin', '2025-11-29 16:12:14', '');
INSERT INTO `sys_menu` VALUES (2007, '充值记录', 2000, 6, 'recharge', 'ai/recharge/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:recharge:list', 'number', 'admin', '2025-11-13 22:38:06', 'admin', '2025-11-13 22:50:05', ''); INSERT INTO `sys_menu` VALUES (2007, '充值记录', 2000, 6, 'recharge', 'ai/recharge/index', NULL, '', 1, 0, 'C', '0', '0', 'ai:recharge:list', 'number', 'admin', '2025-11-13 22:38:06', 'admin', '2025-11-13 22:50:05', '');