feat: 部门余额变更
This commit is contained in:
parent
580e484dd0
commit
c92f531f1b
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,12 @@ export function delDept(deptId) {
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 部门充值/退款
|
||||||
|
export function chargeRefundDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/dept/charge-refund',
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -57,19 +57,31 @@
|
||||||
: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
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -215,6 +227,54 @@
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -244,7 +304,17 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
|
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild, chargeRefundDept } 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 +348,33 @@ 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" }
|
||||||
|
]
|
||||||
|
},
|
||||||
// 表单校验
|
// 表单校验
|
||||||
rules: {
|
rules: {
|
||||||
parentId: [
|
parentId: [
|
||||||
|
|
@ -467,6 +564,87 @@ 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
handleDelete(row) {
|
handleDelete(row) {
|
||||||
this.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
|
this.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ import com.ruoyi.system.service.ISysDeptService;
|
||||||
/**
|
/**
|
||||||
* AI 业务侧部门管理(数据源与系统部门 sys_dept 一致,权限独立)
|
* AI 业务侧部门管理(数据源与系统部门 sys_dept 一致,权限独立)
|
||||||
*/
|
*/
|
||||||
@RestController
|
//@RestController
|
||||||
@RequestMapping("/ai/dept")
|
//@RequestMapping("/ai/dept")
|
||||||
public class AiDeptController extends BaseController
|
public class AiDeptController extends BaseController
|
||||||
{
|
{
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,13 @@ 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.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 +36,9 @@ public class SysDeptController extends BaseController
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISysDeptService deptService;
|
private ISysDeptService deptService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDeptChargeRefundService deptChargeRefundService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取部门列表
|
* 获取部门列表
|
||||||
*/
|
*/
|
||||||
|
|
@ -129,4 +134,18 @@ 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,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,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,5 +1,6 @@
|
||||||
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;
|
||||||
|
|
@ -123,4 +124,18 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -324,6 +325,20 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 递归列表
|
* 递归列表
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue