Merge remote-tracking branch 'origin/seedance_balance' into seedance_balance
This commit is contained in:
commit
ddd2a60f7e
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,4 +49,22 @@ export function delDept(deptId) {
|
||||||
url: '/system/dept/' + deptId,
|
url: '/system/dept/' + 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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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: "请输入 1~100000000 的整数",
|
||||||
|
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("请输入 1~100000000 的整数积分");
|
||||||
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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: "金额须在 0~10000000 之间", trigger: "blur" }
|
||||||
|
],
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: "积分不能为空", trigger: "blur" },
|
||||||
|
{ type: "number", min: 1, max: 100000000, message: "积分须在 1~100000000 之间", 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("积分须在 -100000000~100000000 之间(不含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() {
|
||||||
|
|
|
||||||
|
|
@ -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-用户信息
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>-->
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 递归列表
|
* 递归列表
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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) >= #{amount}
|
||||||
|
</update>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -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', '');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue