feat: AI用户 - 下放/回收积分,去掉设置余额功能。恢复AiDeptController
This commit is contained in:
parent
c92f531f1b
commit
db2052b9fd
|
|
@ -56,18 +56,18 @@ export function changeUserStatus(id, status) {
|
|||
})
|
||||
}
|
||||
|
||||
export function changeBalance(id, balance) {
|
||||
const data = {
|
||||
id,
|
||||
balance
|
||||
}
|
||||
return request({
|
||||
url: '/ai/user/changeBalance',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改余额接口已关闭(与后端 AiUserController.changeBalance 一致停用)
|
||||
// export function changeBalance(id, balance) {
|
||||
// const data = {
|
||||
// id,
|
||||
// balance
|
||||
// }
|
||||
// return request({
|
||||
// url: '/ai/user/changeBalance',
|
||||
// method: 'put',
|
||||
// data: data
|
||||
// })
|
||||
// }
|
||||
|
||||
// 用户状态修改
|
||||
export function updatePassword(id, newPassword) {
|
||||
|
|
@ -90,3 +90,21 @@ export function assignAiUserDept(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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,8 +106,10 @@
|
|||
<el-table-column label="主键ID" align="center" prop="id" width="90" />
|
||||
<el-table-column label="用户ID" align="center" prop="userId" />
|
||||
<el-table-column label="用户账号" align="center" prop="username" />
|
||||
<!--
|
||||
<el-table-column label="上级ID" align="center" prop="superiorUuid" />
|
||||
<el-table-column label="上级账号" align="center" prop="superiorName" />
|
||||
-->
|
||||
<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="email" />
|
||||
|
|
@ -135,7 +137,7 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="余额" align="center" prop="balance" />
|
||||
<el-table-column label="source" align="center" prop="source" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="310">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="460">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
|
|
@ -151,6 +153,21 @@
|
|||
@click="updatePassword(scope.row)"
|
||||
v-hasPermi="['ai:user:edit']"
|
||||
>修改密码</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
|
||||
size="mini"
|
||||
type="text"
|
||||
|
|
@ -158,6 +175,7 @@
|
|||
@click="updateBalance(scope.row)"
|
||||
v-hasPermi="['ai:user:remove']"
|
||||
>修改余额</el-button>
|
||||
-->
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
|
|
@ -263,7 +281,47 @@
|
|||
</div>
|
||||
</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-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="余额" prop="balance">
|
||||
|
|
@ -276,6 +334,7 @@
|
|||
<el-button @click="cancelBalance">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -286,10 +345,11 @@ import {
|
|||
delUser,
|
||||
addUser,
|
||||
updateUser,
|
||||
changeBalance,
|
||||
changeUserStatus,
|
||||
updatePassword,
|
||||
assignAiUserDept
|
||||
assignAiUserDept,
|
||||
issueDeptScore,
|
||||
reclaimDeptScore
|
||||
} from "@/api/ai/user";
|
||||
import { listDept } from "@/api/ai/dept";
|
||||
import Treeselect from "@riophae/vue-treeselect";
|
||||
|
|
@ -320,7 +380,27 @@ export default {
|
|||
// 是否显示弹出层
|
||||
open: 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,
|
||||
deptOptions: [],
|
||||
assignForm: {
|
||||
|
|
@ -337,7 +417,7 @@ export default {
|
|||
phone: null,
|
||||
password: null,
|
||||
openid: null,
|
||||
status: null,
|
||||
status: "0",
|
||||
email: null,
|
||||
birthday: null,
|
||||
invitationCode: null,
|
||||
|
|
@ -417,9 +497,9 @@ export default {
|
|||
case "updatePassword":
|
||||
this.updatePassword(row);
|
||||
break;
|
||||
case "updateBalance":
|
||||
this.updateBalance(row);
|
||||
break;
|
||||
// case "updateBalance":
|
||||
// this.updateBalance(row);
|
||||
// break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -437,15 +517,62 @@ export default {
|
|||
});
|
||||
return;
|
||||
},
|
||||
updateBalance(row) {
|
||||
this.reset();
|
||||
const id = row.id || this.ids;
|
||||
getUser(id).then(response => {
|
||||
this.form = response.data;
|
||||
this.openUpdateBalance = true;
|
||||
this.title = "修改余额";
|
||||
openDeptScoreDialog(row, mode) {
|
||||
if (!row.deptId) {
|
||||
this.$modal.msgWarning("请先分配归属部门后再操作积分");
|
||||
return;
|
||||
}
|
||||
this.deptScoreMode = mode;
|
||||
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-用户信息列表 */
|
||||
getList() {
|
||||
this.loading = true;
|
||||
|
|
@ -475,10 +602,10 @@ export default {
|
|||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
cancelBalance() {
|
||||
this.openUpdateBalance = false;
|
||||
this.reset();
|
||||
},
|
||||
// cancelBalance() {
|
||||
// this.openUpdateBalance = false;
|
||||
// this.reset();
|
||||
// },
|
||||
cancelPassword() {
|
||||
this.openUpdatePassword = false;
|
||||
this.reset();
|
||||
|
|
@ -567,16 +694,16 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
handleChangeBalance() {
|
||||
changeBalance;
|
||||
this.$refs["form"].validate(valid => {
|
||||
changeBalance(this.form.id, this.form.balance).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.openUpdateBalance = false;
|
||||
this.getList();
|
||||
});
|
||||
});
|
||||
},
|
||||
// handleChangeBalance() {
|
||||
// this.$refs["form"].validate(valid => {
|
||||
// if (!valid) return;
|
||||
// changeBalance(this.form.id, this.form.balance).then(() => {
|
||||
// this.$modal.msgSuccess("修改成功");
|
||||
// this.openUpdateBalance = false;
|
||||
// this.getList();
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const ids = row && row.id != null ? row.id : this.ids;
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import com.ruoyi.system.service.ISysDeptService;
|
|||
/**
|
||||
* AI 业务侧部门管理(数据源与系统部门 sys_dept 一致,权限独立)
|
||||
*/
|
||||
//@RestController
|
||||
//@RequestMapping("/ai/dept")
|
||||
@RestController
|
||||
@RequestMapping("/ai/dept")
|
||||
public class AiDeptController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package com.ruoyi.web.controller.ai;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.ruoyi.ai.service.IAiUserService;
|
||||
import com.ruoyi.ai.service.IDeptUserScoreTransferService;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.constant.BalanceChangerConstants;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
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 javax.servlet.http.HttpServletResponse;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import com.ruoyi.common.core.request.ai.AiUserDeptScoreRequest;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* ai-用户信息Controller
|
||||
|
|
@ -36,6 +34,9 @@ public class AiUserController extends BaseController {
|
|||
@Autowired
|
||||
private IAiUserService aiUserService;
|
||||
|
||||
@Autowired
|
||||
private IDeptUserScoreTransferService deptUserScoreTransferService;
|
||||
|
||||
|
||||
/**
|
||||
* 查询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')")
|
||||
@Log(title = "ai-用户信息", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/changeBalance")
|
||||
|
|
@ -130,14 +132,15 @@ public class AiUserController extends BaseController {
|
|||
aiUser.setUpdateBy(getUsername());
|
||||
// 获取原余额
|
||||
AiUser u = aiUserService.selectAiUserById(aiUser.getId());
|
||||
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
|
||||
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
||||
String uuid = java.util.UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
|
||||
String dateTime = new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date());
|
||||
String orderNo = dateTime + uuid;
|
||||
// 计算变更余额
|
||||
BigDecimal amount = NumberUtil.sub(aiUser.getBalance(), u.getBalance());
|
||||
aiUserService.addUserBalance(orderNo, aiUser.getId(), amount, BalanceChangerConstants.SYSTEM_OPERATION);
|
||||
java.math.BigDecimal amount = cn.hutool.core.util.NumberUtil.sub(aiUser.getBalance(), u.getBalance());
|
||||
aiUserService.addUserBalance(orderNo, aiUser.getId(), amount, com.ruoyi.common.constant.BalanceChangerConstants.SYSTEM_OPERATION);
|
||||
return toAjax(1);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 状态密码
|
||||
|
|
@ -149,6 +152,30 @@ public class AiUserController extends BaseController {
|
|||
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-用户信息
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -73,4 +73,14 @@ public class BalanceChangerConstants {
|
|||
*/
|
||||
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;
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ public enum GroupBalanceChangeType {
|
|||
RECHARGE(0, "充值"),
|
||||
REFUND(1, "退款"),
|
||||
ISSUE(2, "下发"),
|
||||
CONSUME(3, "消费"),
|
||||
RECLAIM(3, "回收"),
|
||||
MANUAL_ADJUST(4, "手动修改");
|
||||
|
||||
private final int code;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@
|
|||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 与 ruoyi-framework 对齐,供部门-用户积分下放/回收 Redisson 锁使用 -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>3.17.7</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.byteplus</groupId>-->
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class AiBalanceChangeRecord extends BaseEntity {
|
|||
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 = "操作类型")
|
||||
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")
|
||||
AiOrder selectOneByThirdPartyOrderNum(@Param("id") String id);
|
||||
|
||||
/**
|
||||
* 未完结订单数量:del_flag=0 且 status 非已完成(1)、非失败(2),即进行中(0)等
|
||||
*/
|
||||
int countOpenOrdersByUserId(@Param("userId") Long userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -173,4 +173,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
#{id}
|
||||
</foreach>
|
||||
</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>
|
||||
|
|
@ -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 (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, '');
|
||||
-- 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 (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', '');
|
||||
|
|
|
|||
Loading…
Reference in New Issue