第四阶段开发完成
This commit is contained in:
33
LogBook.md
Normal file
33
LogBook.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
## 2024-07-29 (用户激活码功能 - 后端主体)
|
||||||
|
|
||||||
|
- **核心业务逻辑实现 (Service & Impl)**:
|
||||||
|
- 创建 `ActivationCodeService.java` 接口,定义了激活码生成 (`generateCodes`)、兑换 (`redeemCode`) 和查询条件构建 (`getQueryWrapper`) 的方法。
|
||||||
|
- 创建 `ActivationCodeServiceImpl.java` 实现类:
|
||||||
|
- `generateCodes`: 实现批量生成激活码的逻辑,使用 UUID 生成唯一码,支持自定义数量、面值、过期时间和批次号,并进行批量保存。
|
||||||
|
- `redeemCode`: 实现用户兑换激活码的逻辑,包含参数校验、激活码状态(是否存在、已用、过期)校验,调用 `UserService.increaseBalance()` 增加用户余额,并更新激活码状态。整个过程使用 `@Transactional` 保证事务原子性。
|
||||||
|
- `getQueryWrapper`: 根据 `ActivationCodeQueryRequest` 中的各种条件(如激活码、使用状态、批次号、用户ID、面值范围、过期时间范围、创建时间范围)构建 MyBatis Plus 查询条件,并处理排序(默认按创建时间降序)。
|
||||||
|
|
||||||
|
- **数据传输对象 (DTO) & 视图对象 (VO)**:
|
||||||
|
- 在 `com.yupi.project.model.dto.activationcode` 包下创建了以下 DTO:
|
||||||
|
- `RedeemCodeRequest.java`: 用户兑换激活码请求 (包含 `code`)。
|
||||||
|
- `GenerateCodesRequest.java`: 管理员生成激活码请求 (包含 `count`, `value`, `expireTime`, `batchId`)。
|
||||||
|
- `ActivationCodeQueryRequest.java`: 管理员查询激活码请求,继承自 `PageRequest`,包含多种筛选条件。
|
||||||
|
- 在 `com.yupi.project.model.vo` 包下创建了 `ActivationCodeVO.java`:
|
||||||
|
- 包含激活码详细信息,并为已使用的激活码增加了 `userName` 字段(用于显示使用者用户名),日期时间字段使用 `@JsonFormat` 格式化。
|
||||||
|
|
||||||
|
- **API 控制器 (Controller)**:
|
||||||
|
- 创建 `ActivationCodeController.java`,定义了以下 RESTful API 接口:
|
||||||
|
- `POST /api/activation-code/redeem`: 用户兑换激活码接口。需要用户登录。调用 `activationCodeService.redeemCode`。
|
||||||
|
- `POST /api/activation-code/admin/generate`: 管理员批量生成激活码接口。使用 `@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)` 进行权限控制。调用 `activationCodeService.generateCodes`。
|
||||||
|
- `POST /api/activation-code/admin/list/page`: 管理员分页查询激活码接口。使用 `@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)` 进行权限控制。调用 `activationCodeService.page` 和 `getQueryWrapper`,并实现 `Page<ActivationCode>` 到 `Page<ActivationCodeVO>` 的转换逻辑,包括填充 `userName`。
|
||||||
|
|
||||||
|
- **依赖注入与注解**:
|
||||||
|
- 正确使用了 `@Service`, `@Resource`, `@RestController`, `@RequestMapping`, `@PostMapping`, `@RequestBody`, `@AuthCheck`, `@Transactional`, `@Slf4j` 等注解。
|
||||||
|
|
||||||
|
- **项目结构**: 相关类已放置在规范的包路径下。
|
||||||
|
|
||||||
|
- **待办与后续**:
|
||||||
|
- 编写 API 文档 (Swagger/OpenAPI)。
|
||||||
|
- 编写单元测试和集成测试。
|
||||||
|
- 前端页面对接和开发。
|
||||||
|
- 根据实际测试反馈进一步完善错误处理和日志。
|
||||||
1024
charging_web_app/package-lock.json
generated
1024
charging_web_app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.3",
|
"@headlessui/react": "^2.2.3",
|
||||||
|
"antd": "^5.25.1",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
Button, Card, Col, DatePicker, Form, Input, InputNumber, message,
|
||||||
|
Row, Select, Space, Table, Tabs, Typography, Modal
|
||||||
|
} from 'antd';
|
||||||
|
import { api } from '@/services/api';
|
||||||
|
import { BaseResponse } from '@/types/api';
|
||||||
|
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
// --- TypeScript Interfaces ---
|
||||||
|
interface ActivationCodeVO {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
value: number;
|
||||||
|
isUsed: number;
|
||||||
|
userId?: number | null;
|
||||||
|
userName?: string | null;
|
||||||
|
useTime?: string | null;
|
||||||
|
expireTime?: string | null;
|
||||||
|
batchId?: string | null;
|
||||||
|
createTime: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenerateCodesRequest {
|
||||||
|
count: number;
|
||||||
|
value: number;
|
||||||
|
expireTime?: string | null; // ISO string or null
|
||||||
|
batchId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivationCodeQueryFormData extends ActivationCodeQueryRequest {
|
||||||
|
expireTimeRange?: [any, any]; // Using 'any' for now, ideally Dayjs or Moment tuple
|
||||||
|
createTimeRange?: [any, any];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivationCodeQueryRequest {
|
||||||
|
current?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
code?: string;
|
||||||
|
isUsed?: number;
|
||||||
|
batchId?: string;
|
||||||
|
userId?: number;
|
||||||
|
valueMin?: number;
|
||||||
|
valueMax?: number;
|
||||||
|
expireTimeStart?: string;
|
||||||
|
expireTimeEnd?: string;
|
||||||
|
createTimeStart?: string;
|
||||||
|
createTimeEnd?: string;
|
||||||
|
sortField?: string;
|
||||||
|
sortOrder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Page<T> {
|
||||||
|
records: T[];
|
||||||
|
total: number;
|
||||||
|
current: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdminActivationCodesPage = () => {
|
||||||
|
const { user, isAuthenticated, isLoading: authLoading } = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [generateForm] = Form.useForm<GenerateCodesRequest>();
|
||||||
|
const [queryForm] = Form.useForm<ActivationCodeQueryFormData>();
|
||||||
|
|
||||||
|
const [generatedCodes, setGeneratedCodes] = useState<string[]>([]);
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
|
||||||
|
const [activationCodes, setActivationCodes] = useState<ActivationCodeVO[]>([]);
|
||||||
|
const [totalCodes, setTotalCodes] = useState(0);
|
||||||
|
const [codesLoading, setCodesLoading] = useState(false);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加删除Modal的状态
|
||||||
|
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||||
|
const [currentCodeId, setCurrentCodeId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// 权限检查和重定向
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authLoading) {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
router.replace('/login');
|
||||||
|
} else if (user?.role !== 'admin') {
|
||||||
|
message.error('您没有权限访问此页面。');
|
||||||
|
router.replace('/dashboard'); // 或者其他非管理员默认页
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [authLoading, isAuthenticated, user, router]);
|
||||||
|
|
||||||
|
// --- Generate Codes Logic ---
|
||||||
|
const handleGenerateCodes = async (values: GenerateCodesRequest) => {
|
||||||
|
setIsGenerating(true);
|
||||||
|
setGeneratedCodes([]);
|
||||||
|
try {
|
||||||
|
const requestPayload = {
|
||||||
|
...values,
|
||||||
|
expireTime: values.expireTime ? (values.expireTime as any).toISOString() : null,
|
||||||
|
};
|
||||||
|
const response = await api.post<BaseResponse<string[]>>('/activation-code/admin/generate', requestPayload);
|
||||||
|
if (response.data.code === 0 && response.data.data) {
|
||||||
|
setGeneratedCodes(response.data.data);
|
||||||
|
message.success(`成功生成 ${response.data.data.length} 个激活码!`);
|
||||||
|
generateForm.resetFields();
|
||||||
|
fetchActivationCodes({ current: 1 });
|
||||||
|
} else {
|
||||||
|
message.error(response.data.message || '生成激活码失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.message || error.message || '生成激活码时发生错误');
|
||||||
|
}
|
||||||
|
setIsGenerating(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- List Codes Logic ---
|
||||||
|
const fetchActivationCodes = useCallback(async (params: Partial<ActivationCodeQueryRequest> = {}) => {
|
||||||
|
setCodesLoading(true);
|
||||||
|
try {
|
||||||
|
const formValues = queryForm.getFieldsValue();
|
||||||
|
const queryParams: ActivationCodeQueryRequest = {
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
...formValues,
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formValues.expireTimeRange && formValues.expireTimeRange.length === 2) {
|
||||||
|
queryParams.expireTimeStart = (formValues.expireTimeRange[0] as any).toISOString();
|
||||||
|
queryParams.expireTimeEnd = (formValues.expireTimeRange[1] as any).toISOString();
|
||||||
|
}
|
||||||
|
delete (queryParams as ActivationCodeQueryFormData).expireTimeRange;
|
||||||
|
|
||||||
|
if (formValues.createTimeRange && formValues.createTimeRange.length === 2) {
|
||||||
|
queryParams.createTimeStart = (formValues.createTimeRange[0] as any).toISOString();
|
||||||
|
queryParams.createTimeEnd = (formValues.createTimeRange[1] as any).toISOString();
|
||||||
|
}
|
||||||
|
delete (queryParams as ActivationCodeQueryFormData).createTimeRange;
|
||||||
|
|
||||||
|
const response = await api.post<BaseResponse<Page<ActivationCodeVO>>>('/activation-code/admin/list/page', queryParams);
|
||||||
|
if (response.data.code === 0 && response.data.data) {
|
||||||
|
setActivationCodes(response.data.data.records || []);
|
||||||
|
setTotalCodes(response.data.data.total || 0);
|
||||||
|
} else {
|
||||||
|
message.error(response.data.message || '获取激活码列表失败');
|
||||||
|
setActivationCodes([]);
|
||||||
|
setTotalCodes(0);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.message || error.message || '获取激活码列表时发生错误');
|
||||||
|
setActivationCodes([]);
|
||||||
|
setTotalCodes(0);
|
||||||
|
}
|
||||||
|
setCodesLoading(false);
|
||||||
|
}, [pagination, queryForm]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated && user?.role === 'admin') {
|
||||||
|
fetchActivationCodes();
|
||||||
|
}
|
||||||
|
}, [fetchActivationCodes, isAuthenticated, user]);
|
||||||
|
|
||||||
|
const handleTableChange = (newPagination: any, filters: any, sorter: any) => {
|
||||||
|
const sortParams: Partial<ActivationCodeQueryRequest> = {};
|
||||||
|
if (sorter.field && sorter.order) {
|
||||||
|
sortParams.sortField = sorter.field as string;
|
||||||
|
sortParams.sortOrder = sorter.order;
|
||||||
|
}
|
||||||
|
const newPager = {
|
||||||
|
current: newPagination.current,
|
||||||
|
pageSize: newPagination.pageSize,
|
||||||
|
};
|
||||||
|
setPagination(newPager);
|
||||||
|
fetchActivationCodes({
|
||||||
|
...newPager,
|
||||||
|
...sortParams
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onQueryFinish = (values: ActivationCodeQueryFormData) => {
|
||||||
|
const newPager = {...pagination, current: 1};
|
||||||
|
setPagination(newPager);
|
||||||
|
fetchActivationCodes({current: 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteCode = (codeId: number) => {
|
||||||
|
console.log('handleDeleteCode called with ID:', codeId);
|
||||||
|
setCurrentCodeId(codeId);
|
||||||
|
setIsDeleteModalVisible(true);
|
||||||
|
console.log('Delete modal state set to visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理删除确认
|
||||||
|
const handleDeleteConfirm = () => {
|
||||||
|
if (currentCodeId) {
|
||||||
|
console.log('Delete confirmed for ID:', currentCodeId);
|
||||||
|
|
||||||
|
// 实际执行删除的代码
|
||||||
|
api.post<BaseResponse<boolean>>(
|
||||||
|
`/activation-code/admin/delete`,
|
||||||
|
{ id: currentCodeId }
|
||||||
|
).then(response => {
|
||||||
|
if (response.data.code === 0 && response.data.data === true) {
|
||||||
|
message.success('激活码删除成功!');
|
||||||
|
fetchActivationCodes({ current: pagination.current }); // 刷新当前页
|
||||||
|
} else {
|
||||||
|
message.error(response.data.message || '删除失败');
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
message.error(error.message || '删除激活码时发生网络错误');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsDeleteModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理取消
|
||||||
|
const handleDeleteCancel = () => {
|
||||||
|
console.log('Delete cancelled');
|
||||||
|
setIsDeleteModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Columns for Activation Codes Table ---
|
||||||
|
const columns: any[] = [
|
||||||
|
{ title: 'ID', dataIndex: 'id', key: 'id', sorter: true },
|
||||||
|
{ title: '激活码', dataIndex: 'code', key: 'code' },
|
||||||
|
{ title: '面值', dataIndex: 'value', key: 'value', sorter: true, render: (val: number) => `¥${val.toFixed(2)}` },
|
||||||
|
{
|
||||||
|
title: '状态', dataIndex: 'isUsed', key: 'isUsed', sorter: true,
|
||||||
|
render: (isUsed: number) => isUsed === 1 ? <Text type="danger">已使用</Text> : <Text type="success">未使用</Text>,
|
||||||
|
filters: [
|
||||||
|
{ text: '未使用', value: 0 },
|
||||||
|
{ text: '已使用', value: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ title: '使用者ID', dataIndex: 'userId', key: 'userId', sorter: true },
|
||||||
|
{ title: '使用者', dataIndex: 'userName', key: 'userName' },
|
||||||
|
{ title: '使用时间', dataIndex: 'useTime', key: 'useTime', sorter: true, render: (time: string) => time ? new Date(time).toLocaleString() : '-' },
|
||||||
|
{ title: '过期时间', dataIndex: 'expireTime', key: 'expireTime', sorter: true, render: (time: string) => time ? new Date(time).toLocaleString() : '永不' },
|
||||||
|
{ title: '批次号', dataIndex: 'batchId', key: 'batchId' },
|
||||||
|
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', sorter: true, defaultSortOrder: 'descend', render: (time: string) => new Date(time).toLocaleString() },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render: (text: any, record: ActivationCodeVO) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<Button type="link" danger onClick={() => handleDeleteCode(record.id)}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (authLoading) return <LoadingSpinner />;
|
||||||
|
if (!isAuthenticated || user?.role !== 'admin') return null; // 或显示权限不足的组件
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
<Title level={2} style={{ marginBottom: '24px' }}>激活码管理中心</Title>
|
||||||
|
|
||||||
|
<Tabs defaultActiveKey="1">
|
||||||
|
<TabPane tab="生成激活码" key="1">
|
||||||
|
<Card title="批量生成激活码" style={{ marginBottom: 24 }}>
|
||||||
|
<Form form={generateForm} layout="vertical" onFinish={handleGenerateCodes}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="count" label="生成数量" rules={[{ required: true, message: '请输入生成数量' }]}>
|
||||||
|
<InputNumber min={1} max={1000} style={{ width: '100%' }} placeholder="例如: 100" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="value" label="激活码面值 (元)" rules={[{ required: true, message: '请输入激活码面值' }]}>
|
||||||
|
<InputNumber min={0.01} step={0.01} style={{ width: '100%' }} placeholder="例如: 10.00" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="expireTime" label="过期时间 (可选)">
|
||||||
|
<DatePicker showTime style={{ width: '100%' }} placeholder="选择日期和时间" format="YYYY-MM-DD HH:mm:ss" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="batchId" label="批次号 (可选)">
|
||||||
|
<Input placeholder="例如: spring_promo_2024" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={isGenerating} disabled={isGenerating}>
|
||||||
|
{isGenerating ? '正在生成...' : '立即生成'}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
{generatedCodes.length > 0 && (
|
||||||
|
<Card title="本次生成的激活码" type="inner" style={{ marginTop: 16 }}>
|
||||||
|
<Space direction="vertical" style={{width: '100%'}}>
|
||||||
|
{generatedCodes.map((code, index) => (
|
||||||
|
<Text copyable key={index}>{code}</Text>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
<Text type="secondary" style={{marginTop: 8, display: 'block'}}>共生成 {generatedCodes.length} 个。请妥善保管。</Text>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</TabPane>
|
||||||
|
|
||||||
|
<TabPane tab="管理激活码列表" key="2">
|
||||||
|
<Card title="筛选查询" style={{ marginBottom: 24 }}>
|
||||||
|
<Form form={queryForm} layout="vertical" onFinish={onQueryFinish}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} sm={12} md={6}><Form.Item name="code" label="激活码"><Input placeholder="模糊查询" /></Form.Item></Col>
|
||||||
|
<Col xs={24} sm={12} md={6}>
|
||||||
|
<Form.Item name="isUsed" label="使用状态">
|
||||||
|
<Select allowClear placeholder="全部状态">
|
||||||
|
<Select.Option value={0}>未使用</Select.Option>
|
||||||
|
<Select.Option value={1}>已使用</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={6}><Form.Item name="batchId" label="批次号"><Input placeholder="精确查询" /></Form.Item></Col>
|
||||||
|
<Col xs={24} sm={12} md={6}><Form.Item name="userId" label="使用者ID"><InputNumber style={{ width: '100%' }} placeholder="精确查询" /></Form.Item></Col>
|
||||||
|
<Col xs={24} sm={12} md={6}><Form.Item name="valueMin" label="最小面值"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
|
||||||
|
<Col xs={24} sm={12} md={6}><Form.Item name="valueMax" label="最大面值"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
|
||||||
|
<Col xs={24} sm={12} md={6}><Form.Item name="expireTimeRange" label="过期时间范围">
|
||||||
|
<RangePicker showTime style={{ width: '100%' }} format="YYYY-MM-DD HH:mm:ss" />
|
||||||
|
</Form.Item></Col>
|
||||||
|
<Col xs={24} sm={12} md={6}><Form.Item name="createTimeRange" label="创建时间范围">
|
||||||
|
<RangePicker showTime style={{ width: '100%' }} format="YYYY-MM-DD HH:mm:ss" />
|
||||||
|
</Form.Item></Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={codesLoading}>查询</Button>
|
||||||
|
<Button style={{ marginLeft: 8 }} onClick={() => { queryForm.resetFields(); onQueryFinish({}); }}>重置</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={activationCodes}
|
||||||
|
loading={codesLoading}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={{
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
total: totalCodes,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
|
}}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
|
/>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<Modal
|
||||||
|
title="确认删除"
|
||||||
|
open={isDeleteModalVisible}
|
||||||
|
onOk={handleDeleteConfirm}
|
||||||
|
onCancel={handleDeleteCancel}
|
||||||
|
okText="确认删除"
|
||||||
|
cancelText="取消"
|
||||||
|
okType="danger"
|
||||||
|
>
|
||||||
|
<p>您确定要删除这个激活码 (ID: {currentCodeId}) 吗?此操作不可恢复。</p>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminActivationCodesPage;
|
||||||
@@ -5,8 +5,9 @@ import { useAuth } from '@/contexts/AuthContext';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||||
import { FiUsers, FiCpu, FiBarChart2, FiGrid, FiUserCheck, FiList, FiTerminal, FiSettings } from 'react-icons/fi';
|
import { FiUsers, FiCpu, FiBarChart2, FiGrid, FiUserCheck, FiList, FiTerminal, FiSettings, FiGift } from 'react-icons/fi';
|
||||||
import { api } from '@/utils/axios';
|
import { api } from '@/services/api';
|
||||||
|
import { BaseResponse } from '@/types/api';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
|
||||||
// 匹配后端 AdminDashboardStatsVO
|
// 匹配后端 AdminDashboardStatsVO
|
||||||
@@ -28,13 +29,6 @@ interface AdminStats {
|
|||||||
// availableParkingSpots: number;
|
// availableParkingSpots: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 建议定义一个通用的 BaseResponse 接口
|
|
||||||
interface BackendBaseResponse<T> {
|
|
||||||
data: T;
|
|
||||||
message: string;
|
|
||||||
code?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AdminDashboardPage = () => {
|
const AdminDashboardPage = () => {
|
||||||
const { user, isLoading, isAuthenticated } = useAuth();
|
const { user, isLoading, isAuthenticated } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -56,23 +50,22 @@ const AdminDashboardPage = () => {
|
|||||||
if (isAuthenticated && user?.role === 'admin') {
|
if (isAuthenticated && user?.role === 'admin') {
|
||||||
setStatsLoading(true);
|
setStatsLoading(true);
|
||||||
setStatsError(null);
|
setStatsError(null);
|
||||||
api.get<BackendBaseResponse<AdminStats>>('/admin/stats/summary') // 使用 BackendBaseResponse
|
api.get<BaseResponse<AdminStats>>('/admin/stats/summary')
|
||||||
.then((response: AxiosResponse<BackendBaseResponse<AdminStats>>) => {
|
.then((response: AxiosResponse<BaseResponse<AdminStats>>) => {
|
||||||
if (response.data && response.data.data) {
|
if (response.data && response.data.code === 0 && response.data.data) {
|
||||||
// 后端返回的 totalRevenue 对应前端接口的 totalRevenueToday
|
|
||||||
// 后端返回的 activeSessions 对应前端接口的 totalChargingSessionsToday
|
|
||||||
// 但为了保持一致性,我们最好让前端AdminStats接口的字段名与后端VO的字段名一致
|
|
||||||
// 这里暂时直接使用后端返回的字段名,前端 JSX 部分也需要对应调整
|
|
||||||
setAdminStats(response.data.data);
|
setAdminStats(response.data.data);
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch admin stats or data is null/malformed:", response);
|
console.error("Failed to fetch admin stats or data is null/malformed:", response.data?.message);
|
||||||
setStatsError("加载统计数据失败: 数据为空或格式错误。");
|
setStatsError(response.data?.message || "加载统计数据失败: 数据为空或格式错误。");
|
||||||
setAdminStats(null);
|
setAdminStats(null);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error: AxiosError) => {
|
.catch((error: AxiosError<BaseResponse<unknown>>) => {
|
||||||
console.error("Failed to fetch admin stats:", error);
|
console.error("Failed to fetch admin stats:", error);
|
||||||
if (error.response) {
|
const backendMessage = error.response?.data?.message;
|
||||||
|
if (backendMessage) {
|
||||||
|
setStatsError(`加载统计数据失败: ${backendMessage}`);
|
||||||
|
} else if (error.response) {
|
||||||
setStatsError(`加载统计数据失败: ${error.response.status} ${error.response.statusText}`);
|
setStatsError(`加载统计数据失败: ${error.response.status} ${error.response.statusText}`);
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
setStatsError("加载统计数据失败: 未收到服务器响应。");
|
setStatsError("加载统计数据失败: 未收到服务器响应。");
|
||||||
@@ -200,7 +193,6 @@ const AdminDashboardPage = () => {
|
|||||||
description="查看所有充电会话记录、详情"
|
description="查看所有充电会话记录、详情"
|
||||||
href="/admin/sessions"
|
href="/admin/sessions"
|
||||||
icon={<FiList size={28}/>}
|
icon={<FiList size={28}/>}
|
||||||
disabled={true}
|
|
||||||
/>
|
/>
|
||||||
<NavCard
|
<NavCard
|
||||||
title="用户管理"
|
title="用户管理"
|
||||||
@@ -208,6 +200,12 @@ const AdminDashboardPage = () => {
|
|||||||
href="/admin/user-management"
|
href="/admin/user-management"
|
||||||
icon={<FiUserCheck size={28}/>}
|
icon={<FiUserCheck size={28}/>}
|
||||||
/>
|
/>
|
||||||
|
<NavCard
|
||||||
|
title="激活码管理"
|
||||||
|
description="生成、查询和管理激活码"
|
||||||
|
href="/admin/activation-codes"
|
||||||
|
icon={<FiGift size={28}/>}
|
||||||
|
/>
|
||||||
<NavCard
|
<NavCard
|
||||||
title="系统设置"
|
title="系统设置"
|
||||||
description="配置系统参数、费率等"
|
description="配置系统参数、费率等"
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import { useAuth } from '@/contexts/AuthContext';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||||
import { api, BaseResponse } from '@/utils/axios'; // Import the api instance
|
import { api } from '@/services/api'; // 新的API实例导入
|
||||||
import { FiDollarSign, FiZap, FiClock, FiList, FiChevronRight, FiInfo, FiPower, FiAlertCircle } from 'react-icons/fi';
|
import { BaseResponse } from '@/types/api'; // 新的BaseResponse导入
|
||||||
|
import { FiDollarSign, FiZap, FiClock, FiList, FiChevronRight, FiInfo, FiPower, FiAlertCircle, FiGift } from 'react-icons/fi';
|
||||||
|
import { Button, Modal, message } from 'antd'; // 确保Button和Modal已导入
|
||||||
|
import RedeemCodeForm from '@/components/RedeemCodeForm'; // 导入兑换表单组件
|
||||||
|
|
||||||
// Interface for active charging session (matches ChargingSessionVO fields used)
|
// Interface for active charging session (matches ChargingSessionVO fields used)
|
||||||
interface ActiveChargingSession {
|
interface ActiveChargingSession {
|
||||||
@@ -26,7 +29,7 @@ interface UserDashboardStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth();
|
const { user, isAuthenticated, isLoading: authLoading, logout, checkAuth } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [activeSession, setActiveSession] = useState<ActiveChargingSession | null | undefined>(undefined);
|
const [activeSession, setActiveSession] = useState<ActiveChargingSession | null | undefined>(undefined);
|
||||||
@@ -34,6 +37,7 @@ export default function DashboardPage() {
|
|||||||
const [dataLoading, setDataLoading] = useState(true);
|
const [dataLoading, setDataLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isStopping, setIsStopping] = useState(false);
|
const [isStopping, setIsStopping] = useState(false);
|
||||||
|
const [isRedeemModalVisible, setIsRedeemModalVisible] = useState(false); // Modal状态
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading && !isAuthenticated) {
|
if (!authLoading && !isAuthenticated) {
|
||||||
@@ -51,44 +55,30 @@ export default function DashboardPage() {
|
|||||||
setDataLoading(true);
|
setDataLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
// 使用 Promise.allSettled 来处理部分请求失败的情况,如果需要
|
|
||||||
const [sessionResult, statsResult] = await Promise.all([
|
const [sessionResult, statsResult] = await Promise.all([
|
||||||
api.get<BaseResponse<ActiveChargingSession | null>>('/session/my/active'),
|
api.get<BaseResponse<ActiveChargingSession | null>>('/session/my/active'),
|
||||||
api.get<BaseResponse<UserDashboardStats>>('/user/stats/mine')
|
api.get<BaseResponse<UserDashboardStats>>('/user/stats/mine')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (sessionResult.data && sessionResult.data.code === 0) { // 假设 code 0 代表成功
|
if (sessionResult.data && sessionResult.data.code === 0) {
|
||||||
console.log("Active session response:", sessionResult.data);
|
setActiveSession(sessionResult.data.data);
|
||||||
setActiveSession(sessionResult.data.data); // data 可以是 null
|
|
||||||
} else {
|
} else {
|
||||||
// setActiveSession(null); // 或者根据后端错误信息设置
|
setActiveSession(null);
|
||||||
console.warn("Failed to fetch active session or no active session:", sessionResult.data?.message);
|
|
||||||
setActiveSession(null); // 如果没有活动会话,后端data.data可能为null,是正常情况
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statsResult.data && statsResult.data.code === 0 && statsResult.data.data) {
|
if (statsResult.data && statsResult.data.code === 0 && statsResult.data.data) {
|
||||||
console.log("User stats response:", statsResult.data.data);
|
|
||||||
// 检查后端返回的是monthlyCharges还是monthlySessions,并进行适当转换
|
|
||||||
const statsData = statsResult.data.data;
|
const statsData = statsResult.data.data;
|
||||||
if ('monthlyCharges' in statsData && statsData.monthlyCharges !== undefined) {
|
setUserStats({
|
||||||
// 如果后端返回的是monthlyCharges字段,映射到monthlySessions
|
monthlySessions: (statsData as any).monthlyCharges !== undefined ? (statsData as any).monthlyCharges : statsData.monthlySessions,
|
||||||
setUserStats({
|
monthlySpending: statsData.monthlySpending
|
||||||
monthlySessions: statsData.monthlyCharges as number,
|
});
|
||||||
monthlySpending: statsData.monthlySpending
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 否则假定数据结构已匹配接口
|
|
||||||
setUserStats(statsData);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setUserStats(null);
|
setUserStats(null);
|
||||||
console.error("Failed to fetch user stats:", statsResult.data?.message);
|
|
||||||
// setError(prev => prev ? prev + '\n' + (statsResult.data?.message || '获取用户统计失败') : (statsResult.data?.message || '获取用户统计失败'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Error fetching dashboard data:", err);
|
console.error("Error fetching dashboard data:", err);
|
||||||
const errorMessage = err.response?.data?.message || err.message || '获取仪表盘数据失败,请稍后再试。';
|
const errorMessage = err.response?.data?.message || err.message || '获取仪表盘数据失败';
|
||||||
setError(prevError => prevError ? `${prevError}\n${errorMessage}` : errorMessage);
|
setError(prevError => prevError ? `${prevError}\n${errorMessage}` : errorMessage);
|
||||||
setActiveSession(null);
|
setActiveSession(null);
|
||||||
setUserStats(null);
|
setUserStats(null);
|
||||||
@@ -100,15 +90,14 @@ export default function DashboardPage() {
|
|||||||
if (isAuthenticated && user?.role === 'user') {
|
if (isAuthenticated && user?.role === 'user') {
|
||||||
fetchData();
|
fetchData();
|
||||||
} else if (!authLoading && !isAuthenticated) {
|
} else if (!authLoading && !isAuthenticated) {
|
||||||
// Handle cases where user is not authenticated and not loading
|
setDataLoading(false);
|
||||||
setDataLoading(false); // Stop loading if not fetching
|
|
||||||
}
|
}
|
||||||
}, [fetchData, isAuthenticated, user, authLoading]);
|
}, [fetchData, isAuthenticated, user, authLoading]);
|
||||||
|
|
||||||
// 直接从仪表盘停止充电
|
// 直接从仪表盘停止充电
|
||||||
const handleStopCharging = async () => {
|
const handleStopCharging = async () => {
|
||||||
if (!activeSession || !activeSession.id) {
|
if (!activeSession || !activeSession.id) {
|
||||||
setError("无法停止充电:无效的会话信息。");
|
message.error("无法停止充电:无效的会话信息。"); // 使用 antd message
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsStopping(true);
|
setIsStopping(true);
|
||||||
@@ -116,14 +105,17 @@ export default function DashboardPage() {
|
|||||||
try {
|
try {
|
||||||
const response = await api.post<BaseResponse<any>>(`/session/stop`, { sessionId: activeSession.id });
|
const response = await api.post<BaseResponse<any>>(`/session/stop`, { sessionId: activeSession.id });
|
||||||
if (response.data && response.data.code === 0) {
|
if (response.data && response.data.code === 0) {
|
||||||
// 停止成功后重新获取数据
|
|
||||||
fetchData();
|
fetchData();
|
||||||
|
message.success('已成功发送停止充电请求。');
|
||||||
} else {
|
} else {
|
||||||
setError(response.data?.message || "停止充电请求失败");
|
setError(response.data?.message || "停止充电请求失败");
|
||||||
|
message.error(response.data?.message || "停止充电请求失败");
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Error stopping charging session:", err);
|
console.error("Error stopping charging session:", err);
|
||||||
setError(err.response?.data?.message || err.message || "停止充电时发生错误");
|
const msg = err.response?.data?.message || err.message || "停止充电时发生错误";
|
||||||
|
setError(msg);
|
||||||
|
message.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setIsStopping(false);
|
setIsStopping(false);
|
||||||
}
|
}
|
||||||
@@ -168,19 +160,32 @@ export default function DashboardPage() {
|
|||||||
return '--';
|
return '--';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRedeemSuccess = () => {
|
||||||
|
setIsRedeemModalVisible(false);
|
||||||
|
checkAuth(); // 刷新用户数据,尤其是余额
|
||||||
|
// fetchData(); // 也可以选择刷新整个仪表盘数据,如果余额不通过checkAuth更新
|
||||||
|
message.success('激活码已成功兑换,余额已更新!');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 p-4 md:p-8">
|
<div className="min-h-screen bg-gray-100 p-4 md:p-8">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<header className="mb-8 flex justify-between items-center">
|
<header className="mb-8 flex justify-between items-center">
|
||||||
<h1 className="text-3xl font-bold text-gray-800">用户中心</h1>
|
<h1 className="text-3xl font-bold text-gray-800">用户中心</h1>
|
||||||
{/* Logout button is in global layout, no need here if using that strategy */}
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<FiGift className="mr-2" />}
|
||||||
|
onClick={() => setIsRedeemModalVisible(true)}
|
||||||
|
>
|
||||||
|
兑换激活码
|
||||||
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-xl shadow-lg mb-8">
|
<div className="bg-white p-6 rounded-xl shadow-lg mb-8">
|
||||||
<h2 className="text-2xl font-semibold text-gray-700 mb-2">欢迎, {user.username}!</h2>
|
<h2 className="text-2xl font-semibold text-gray-700 mb-2">欢迎, {user.username}!</h2>
|
||||||
<p className="text-gray-600 mb-1"><span className="font-medium">用户ID:</span> {user.id}</p>
|
<p className="text-gray-600 mb-1"><span className="font-medium">用户ID:</span> {user.id}</p>
|
||||||
<p className="text-gray-600 mb-1"><span className="font-medium">角色:</span> {user.role === 'user' ? '普通用户' : user.role}</p>
|
<p className="text-gray-600 mb-1"><span className="font-medium">角色:</span> {user.role === 'user' ? '普通用户' : user.role}</p>
|
||||||
<p className="text-gray-600"><span className="font-medium">账户余额:</span> ¥{user.balance?.toFixed(2) ?? 'N/A'}</p>
|
<p className="text-gray-600"><span className="font-medium">账户余额:</span> <span className="text-lg font-semibold text-green-600">¥{user.balance?.toFixed(2) ?? 'N/A'}</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -286,6 +291,20 @@ export default function DashboardPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="兑换激活码"
|
||||||
|
open={isRedeemModalVisible}
|
||||||
|
onCancel={() => setIsRedeemModalVisible(false)}
|
||||||
|
footer={null} // RedeemCodeForm 内部有自己的提交和取消逻辑
|
||||||
|
destroyOnClose // 关闭时销毁内部组件状态
|
||||||
|
>
|
||||||
|
<RedeemCodeForm
|
||||||
|
onSuccess={handleRedeemSuccess}
|
||||||
|
onCancel={() => setIsRedeemModalVisible(false)} // Modal 的取消按钮也调用此 handler
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
107
charging_web_app/src/app/(authenticated)/redeem-code/page.tsx
Normal file
107
charging_web_app/src/app/(authenticated)/redeem-code/page.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import React, { useState, ChangeEvent, FormEvent } from 'react';
|
||||||
|
import { Input, Button, Card, Typography, message } from 'antd';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { api as apiService } from '@/services/api';
|
||||||
|
|
||||||
|
// 暂时在此处定义 BaseResponse,因为 @/services/api.ts 未导出它
|
||||||
|
// 理想情况下,它应该在全局类型定义文件中或与api实例一起导出
|
||||||
|
export interface BaseResponse<T> {
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
|
||||||
|
const RedeemActivationCodePage = () => {
|
||||||
|
const [code, setCode] = useState<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { isAuthenticated, user } = useAuth();
|
||||||
|
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setCode(e.target.value.trim());
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
message.error('请先登录后再兑换激活码。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
setError('请输入激活码。');
|
||||||
|
message.error('请输入激活码。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiService.post<BaseResponse<boolean>>('/api/activation-code/redeem',
|
||||||
|
{ code }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data && response.data.code === 0 && response.data.data === true) {
|
||||||
|
message.success('激活码兑换成功!您的余额已更新。');
|
||||||
|
setCode('');
|
||||||
|
} else {
|
||||||
|
const errorMessage = response.data?.message || '兑换失败,请稍后再试。';
|
||||||
|
setError(errorMessage);
|
||||||
|
message.error(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Redeem code error:', err);
|
||||||
|
const errorMessage = err.response?.data?.message || err.message || '兑换过程中发生错误,请检查网络或联系管理员。';
|
||||||
|
setError(errorMessage);
|
||||||
|
message.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '500px', margin: '50px auto', padding: '20px' }}>
|
||||||
|
<Card bordered={false}>
|
||||||
|
<Title level={3} style={{ textAlign: 'center', marginBottom: '30px' }}>
|
||||||
|
兑换激活码
|
||||||
|
</Title>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入您的激活码"
|
||||||
|
value={code}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
size="large"
|
||||||
|
style={{ marginBottom: '20px' }}
|
||||||
|
disabled={isLoading || !isAuthenticated}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<Typography.Text type="danger" style={{ display: 'block', marginBottom: '10px' }}>
|
||||||
|
{error}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={!isAuthenticated}
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{isLoading ? '兑换中...' : '立即兑换'}
|
||||||
|
</Button>
|
||||||
|
{!isAuthenticated && (
|
||||||
|
<Typography.Text type="warning" style={{ display: 'block', marginTop: '10px', textAlign: 'center' }}>
|
||||||
|
请先登录以兑换激活码。
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RedeemActivationCodePage;
|
||||||
109
charging_web_app/src/components/RedeemCodeForm.tsx
Normal file
109
charging_web_app/src/components/RedeemCodeForm.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useState, ChangeEvent, FormEvent } from 'react';
|
||||||
|
import { Input, Button, Typography, message } from 'antd';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { api as apiService } from '@/services/api';
|
||||||
|
import { BaseResponse } from '@/types/api'; // 假设 BaseResponse 移到了这里
|
||||||
|
|
||||||
|
interface RedeemCodeFormProps {
|
||||||
|
onSuccess?: () => void; // 兑换成功后的回调
|
||||||
|
onCancel?: () => void; // 可选的取消/关闭回调
|
||||||
|
}
|
||||||
|
|
||||||
|
const RedeemCodeForm: React.FC<RedeemCodeFormProps> = ({ onSuccess, onCancel }) => {
|
||||||
|
const [code, setCode] = useState<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { isAuthenticated, checkAuth } = useAuth(); // checkAuth 用于成功后刷新用户信息
|
||||||
|
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setCode(e.target.value.trim());
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e?: FormEvent<HTMLFormElement>) => {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
message.error('请先登录后再兑换激活码。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!code) {
|
||||||
|
setError('请输入激活码。');
|
||||||
|
message.error('请输入激活码。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiService.post<BaseResponse<boolean>>('/activation-code/redeem',
|
||||||
|
{ code }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Backend response for redeem:', response.data);
|
||||||
|
|
||||||
|
if (response.data && response.data.code === 0 && response.data.data === true) {
|
||||||
|
message.success('激活码兑换成功!您的余额已更新。');
|
||||||
|
setCode('');
|
||||||
|
await checkAuth(); // 刷新用户信息,特别是余额
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
} else {
|
||||||
|
const errorMessage = response.data?.message || '兑换失败,请稍后再试。';
|
||||||
|
setError(errorMessage);
|
||||||
|
message.error(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Redeem code error:', err);
|
||||||
|
const errorMessage = err.response?.data?.message || err.message || '兑换过程中发生错误。';
|
||||||
|
setError(errorMessage);
|
||||||
|
message.error(errorMessage);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入您的激活码"
|
||||||
|
value={code}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
size="large"
|
||||||
|
style={{ marginBottom: '20px' }}
|
||||||
|
disabled={isLoading || !isAuthenticated}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<Typography.Text type="danger" style={{ display: 'block', marginBottom: '10px' }}>
|
||||||
|
{error}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={!isAuthenticated || !code.trim()}
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
style={{ marginBottom: onCancel ? '10px' : '0' }} // 如果有取消按钮,给兑换按钮留点空间
|
||||||
|
>
|
||||||
|
{isLoading ? '兑换中...' : '立即兑换'}
|
||||||
|
</Button>
|
||||||
|
{onCancel && (
|
||||||
|
<Button
|
||||||
|
onClick={onCancel}
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isAuthenticated && (
|
||||||
|
<Typography.Text type="warning" style={{ display: 'block', marginTop: '10px', textAlign: 'center' }}>
|
||||||
|
请先登录以兑换激活码。
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RedeemCodeForm;
|
||||||
@@ -44,6 +44,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
const response = await api.get('/user/current');
|
const response = await api.get('/user/current');
|
||||||
if (response.data.code === 0 && response.data.data) {
|
if (response.data.code === 0 && response.data.data) {
|
||||||
|
console.log('Data from /user/current for setUser:', JSON.stringify(response.data.data));
|
||||||
setUser(response.data.data);
|
setUser(response.data.data);
|
||||||
console.log('User authenticated:', response.data.data.username, 'Role:', response.data.data.role);
|
console.log('User authenticated:', response.data.data.username, 'Role:', response.data.data.role);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
10
charging_web_app/src/types/api.ts
Normal file
10
charging_web_app/src/types/api.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 通用的后端响应体结构
|
||||||
|
*/
|
||||||
|
export interface BaseResponse<T> {
|
||||||
|
code: number; // 响应状态码,0表示成功
|
||||||
|
data: T; // 泛型数据,可以是任何类型
|
||||||
|
message?: string; // 可选的消息说明
|
||||||
|
}
|
||||||
|
|
||||||
|
// 你可以在这里添加其他通用的API相关类型定义
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package com.yupi.project.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.yupi.project.annotation.AuthCheck;
|
||||||
|
import com.yupi.project.common.BaseResponse;
|
||||||
|
import com.yupi.project.common.DeleteRequest;
|
||||||
|
import com.yupi.project.common.ErrorCode;
|
||||||
|
import com.yupi.project.common.ResultUtils;
|
||||||
|
import com.yupi.project.constant.UserConstant;
|
||||||
|
import com.yupi.project.exception.BusinessException;
|
||||||
|
import com.yupi.project.exception.ThrowUtils;
|
||||||
|
import com.yupi.project.model.dto.activationcode.ActivationCodeQueryRequest;
|
||||||
|
import com.yupi.project.model.dto.activationcode.GenerateCodesRequest;
|
||||||
|
import com.yupi.project.model.dto.activationcode.RedeemCodeRequest;
|
||||||
|
import com.yupi.project.model.dto.common.IdRequest;
|
||||||
|
import com.yupi.project.model.entity.ActivationCode;
|
||||||
|
import com.yupi.project.model.entity.User;
|
||||||
|
import com.yupi.project.model.vo.ActivationCodeVO;
|
||||||
|
import com.yupi.project.service.ActivationCodeService;
|
||||||
|
import com.yupi.project.service.UserService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/activation-code")
|
||||||
|
@Slf4j
|
||||||
|
public class ActivationCodeController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ActivationCodeService activationCodeService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
// region 用户操作
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户兑换激活码
|
||||||
|
*
|
||||||
|
* @param redeemCodeRequest 激活码请求
|
||||||
|
* @param request HTTP请求
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/redeem")
|
||||||
|
public BaseResponse<Boolean> redeemActivationCode(@RequestBody RedeemCodeRequest redeemCodeRequest, HttpServletRequest request) {
|
||||||
|
if (redeemCodeRequest == null || redeemCodeRequest.getCode() == null || redeemCodeRequest.getCode().trim().isEmpty()) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "激活码不能为空");
|
||||||
|
}
|
||||||
|
User loginUser = userService.getCurrentUser(request);
|
||||||
|
if (loginUser == null) {
|
||||||
|
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户未登录或登录已失效");
|
||||||
|
}
|
||||||
|
boolean result = activationCodeService.redeemCode(loginUser.getId(), redeemCodeRequest.getCode());
|
||||||
|
return ResultUtils.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region 管理员操作
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员批量生成激活码
|
||||||
|
*
|
||||||
|
* @param generateCodesRequest 生成请求
|
||||||
|
* @return 生成的激活码列表 (只返回code,避免信息过多,或根据需要返回VO)
|
||||||
|
*/
|
||||||
|
@PostMapping("/admin/generate")
|
||||||
|
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||||
|
public BaseResponse<List<ActivationCodeVO>> generateActivationCodes(@RequestBody GenerateCodesRequest generateCodesRequest) {
|
||||||
|
if (generateCodesRequest == null) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
if (generateCodesRequest.getCount() == null || generateCodesRequest.getCount() <= 0) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成数量必须大于0");
|
||||||
|
}
|
||||||
|
if (generateCodesRequest.getValue() == null || generateCodesRequest.getValue().signum() <= 0) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "面值必须大于0");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<ActivationCode> codes = activationCodeService.generateCodes(
|
||||||
|
generateCodesRequest.getCount(),
|
||||||
|
generateCodesRequest.getValue(),
|
||||||
|
generateCodesRequest.getExpireTime(),
|
||||||
|
generateCodesRequest.getBatchId()
|
||||||
|
);
|
||||||
|
List<ActivationCodeVO> voList = codes.stream()
|
||||||
|
.map(code -> {
|
||||||
|
ActivationCodeVO vo = new ActivationCodeVO();
|
||||||
|
BeanUtils.copyProperties(code, vo);
|
||||||
|
return vo;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return ResultUtils.success(voList);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error generating activation codes", e);
|
||||||
|
throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员分页查询激活码
|
||||||
|
*
|
||||||
|
* @param activationCodeQueryRequest 查询条件
|
||||||
|
* @return 分页的激活码视图对象
|
||||||
|
*/
|
||||||
|
@PostMapping("/admin/list/page")
|
||||||
|
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||||
|
public BaseResponse<Page<ActivationCodeVO>> listActivationCodesByPage(@RequestBody ActivationCodeQueryRequest activationCodeQueryRequest) {
|
||||||
|
if (activationCodeQueryRequest == null) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
long current = activationCodeQueryRequest.getCurrent();
|
||||||
|
long size = activationCodeQueryRequest.getPageSize();
|
||||||
|
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
||||||
|
|
||||||
|
Page<ActivationCode> activationCodePage = activationCodeService.page(new Page<>(current, size),
|
||||||
|
activationCodeService.getQueryWrapper(activationCodeQueryRequest));
|
||||||
|
|
||||||
|
Page<ActivationCodeVO> activationCodeVOPage = new Page<>(activationCodePage.getCurrent(), activationCodePage.getSize(), activationCodePage.getTotal());
|
||||||
|
List<ActivationCodeVO> activationCodeVOList = activationCodePage.getRecords().stream().map(activationCode -> {
|
||||||
|
ActivationCodeVO activationCodeVO = new ActivationCodeVO();
|
||||||
|
BeanUtils.copyProperties(activationCode, activationCodeVO);
|
||||||
|
if (activationCode.getIsUsed() == 1 && activationCode.getUserId() != null) {
|
||||||
|
User user = userService.getById(activationCode.getUserId());
|
||||||
|
if (user != null) {
|
||||||
|
activationCodeVO.setUserName(user.getUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return activationCodeVO;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
activationCodeVOPage.setRecords(activationCodeVOList);
|
||||||
|
|
||||||
|
return ResultUtils.success(activationCodeVOPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员删除激活码
|
||||||
|
*
|
||||||
|
* @param idRequest 包含激活码ID的请求体
|
||||||
|
* @param request HTTP请求 (用于权限校验等)
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/admin/delete")
|
||||||
|
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||||
|
public BaseResponse<Boolean> deleteActivationCode(@RequestBody IdRequest idRequest, HttpServletRequest request) {
|
||||||
|
if (idRequest == null || idRequest.getId() == null || idRequest.getId() <= 0) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "激活码ID不能为空");
|
||||||
|
}
|
||||||
|
boolean result = activationCodeService.deleteCode(idRequest.getId());
|
||||||
|
return ResultUtils.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.yupi.project.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.yupi.project.model.entity.ActivationCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 针对表【activation_code(激活码表)】的数据库操作Mapper
|
||||||
|
* @createDate 2024-08-04 10:00:00
|
||||||
|
* @Entity com.yupi.project.model.entity.ActivationCode
|
||||||
|
*/
|
||||||
|
public interface ActivationCodeMapper extends BaseMapper<ActivationCode> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.yupi.project.model.dto.activationcode;
|
||||||
|
|
||||||
|
import com.yupi.project.common.PageRequest;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码查询请求
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class ActivationCodeQueryRequest extends PageRequest implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码 (模糊匹配)
|
||||||
|
*/
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已使用 (0 - 未使用, 1 - 已使用)
|
||||||
|
*/
|
||||||
|
private Integer isUsed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批次号
|
||||||
|
*/
|
||||||
|
private String batchId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用者用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面值下限 (用于范围查询)
|
||||||
|
*/
|
||||||
|
private BigDecimal valueMin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面值上限 (用于范围查询)
|
||||||
|
*/
|
||||||
|
private BigDecimal valueMax;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间开始 (用于范围查询)
|
||||||
|
*/
|
||||||
|
private LocalDateTime expireTimeStart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间结束 (用于范围查询)
|
||||||
|
*/
|
||||||
|
private LocalDateTime expireTimeEnd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间开始 (用于范围查询)
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTimeStart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间结束 (用于范围查询)
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTimeEnd;
|
||||||
|
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.yupi.project.model.dto.activationcode;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员生成激活码请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class GenerateCodesRequest implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成数量
|
||||||
|
*/
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码面值
|
||||||
|
*/
|
||||||
|
private BigDecimal value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间 (可选, null 表示永不过期)
|
||||||
|
*/
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批次号 (可选, 用于追踪)
|
||||||
|
*/
|
||||||
|
private String batchId;
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.yupi.project.model.dto.activationcode;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户兑换激活码请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RedeemCodeRequest implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码
|
||||||
|
*/
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.yupi.project.model.dto.common;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用ID请求DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class IdRequest implements Serializable {
|
||||||
|
private Long id;
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.yupi.project.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码表实体类
|
||||||
|
*/
|
||||||
|
@TableName(value = "activation_code")
|
||||||
|
@Data
|
||||||
|
public class ActivationCode implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码字符串,确保全局唯一
|
||||||
|
*/
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码对应的面值(金额)
|
||||||
|
*/
|
||||||
|
private BigDecimal value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已使用:0-未使用,1-已使用
|
||||||
|
*/
|
||||||
|
private Integer isUsed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用者用户ID (如果已使用)
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码使用时间 (如果已使用)
|
||||||
|
*/
|
||||||
|
private LocalDateTime useTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码过期时间 (NULL表示永不过期)
|
||||||
|
*/
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成批次号,方便管理员追踪管理
|
||||||
|
*/
|
||||||
|
private String batchId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑删除标志:0-未删除,1-已删除
|
||||||
|
*/
|
||||||
|
@TableLogic
|
||||||
|
private Integer isDelete;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.yupi.project.model.vo;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码视图对象
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ActivationCodeVO implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码
|
||||||
|
*/
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面值
|
||||||
|
*/
|
||||||
|
private BigDecimal value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已使用 (0 - 未使用, 1 - 已使用)
|
||||||
|
*/
|
||||||
|
private Integer isUsed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用者用户ID (如果已使用)
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用者用户名 (如果已使用, 需要额外查询填充)
|
||||||
|
*/
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用时间 (如果已使用)
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime useTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批次号
|
||||||
|
*/
|
||||||
|
private String batchId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.yupi.project.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.yupi.project.model.entity.ActivationCode;
|
||||||
|
import com.yupi.project.model.dto.activationcode.ActivationCodeQueryRequest;
|
||||||
|
// TODO: Import Page, ActivationCodeVO, ActivationCodeQueryRequest when created
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活码服务接口
|
||||||
|
*/
|
||||||
|
public interface ActivationCodeService extends IService<ActivationCode> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (管理员) 批量生成激活码。
|
||||||
|
*
|
||||||
|
* @param count 生成数量
|
||||||
|
* @param value 单个激活码的面值
|
||||||
|
* @param expireTime 过期时间 (null 表示永不过期)
|
||||||
|
* @param batchId 批次ID (可由调用者传入或在Service中生成)
|
||||||
|
* @return 生成的激活码列表
|
||||||
|
* @throws Exception 如果生成过程中发生错误
|
||||||
|
*/
|
||||||
|
List<ActivationCode> generateCodes(int count, BigDecimal value, LocalDateTime expireTime, String batchId) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (用户) 兑换激活码。
|
||||||
|
*
|
||||||
|
* @param currentUserId 当前登录用户的ID
|
||||||
|
* @param code 激活码字符串
|
||||||
|
* @return true 如果兑换成功,并增加了用户余额
|
||||||
|
* @throws com.yupi.project.exception.BusinessException 如果激活码无效、已使用、已过期或兑换失败
|
||||||
|
*/
|
||||||
|
boolean redeemCode(Long currentUserId, String code);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建查询条件
|
||||||
|
*
|
||||||
|
* @param activationCodeQueryRequest 查询请求
|
||||||
|
* @return QueryWrapper
|
||||||
|
*/
|
||||||
|
QueryWrapper<ActivationCode> getQueryWrapper(ActivationCodeQueryRequest activationCodeQueryRequest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID删除激活码 (逻辑删除)
|
||||||
|
*
|
||||||
|
* @param codeId 激活码ID
|
||||||
|
* @return 是否删除成功
|
||||||
|
*/
|
||||||
|
boolean deleteCode(Long codeId);
|
||||||
|
|
||||||
|
// TODO: Add method for listing codes (admin)
|
||||||
|
// Page<ActivationCodeVO> listCodes(ActivationCodeQueryRequest queryRequest);
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package com.yupi.project.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.yupi.project.common.ErrorCode;
|
||||||
|
import com.yupi.project.exception.BusinessException;
|
||||||
|
import com.yupi.project.mapper.ActivationCodeMapper;
|
||||||
|
import com.yupi.project.model.dto.activationcode.ActivationCodeQueryRequest;
|
||||||
|
import com.yupi.project.model.entity.ActivationCode;
|
||||||
|
import com.yupi.project.service.ActivationCodeService;
|
||||||
|
import com.yupi.project.service.UserService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class ActivationCodeServiceImpl extends ServiceImpl<ActivationCodeMapper, ActivationCode> implements ActivationCodeService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
// 通常不直接注入Mapper,而是通过继承的ServiceImpl的方法操作,但如果需要复杂查询可以注入
|
||||||
|
// @Resource
|
||||||
|
// private ActivationCodeMapper activationCodeMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public List<ActivationCode> generateCodes(int count, BigDecimal value, LocalDateTime expireTime, String batchId) throws Exception {
|
||||||
|
if (count <= 0) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成数量必须大于0");
|
||||||
|
}
|
||||||
|
if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "激活码面值必须大于0");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ActivationCode> generatedCodes = new ArrayList<>();
|
||||||
|
String effectiveBatchId = (batchId == null || batchId.isEmpty()) ? UUID.randomUUID().toString() : batchId;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
ActivationCode activationCode = new ActivationCode();
|
||||||
|
// 生成唯一code的简单策略,实际项目中可能需要更复杂的防碰撞机制或预生成库
|
||||||
|
String uniqueCode = UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
|
||||||
|
// TODO: 考虑code冲突的可能性,虽然UUID冲突概率极低,但严格来说需要循环检查或数据库唯一约束处理
|
||||||
|
// 在高并发下,先生成再批量插入,并处理唯一约束异常可能是更好的方式
|
||||||
|
|
||||||
|
activationCode.setCode(uniqueCode);
|
||||||
|
activationCode.setValue(value);
|
||||||
|
activationCode.setExpireTime(expireTime);
|
||||||
|
activationCode.setBatchId(effectiveBatchId);
|
||||||
|
activationCode.setIsUsed(0); // 0 for not used
|
||||||
|
activationCode.setIsDelete(0); // 0 for not deleted
|
||||||
|
// createTime and updateTime will be handled by DB default or MyBatis Plus fill strategy
|
||||||
|
generatedCodes.add(activationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean saveBatchResult = this.saveBatch(generatedCodes);
|
||||||
|
if (!saveBatchResult) {
|
||||||
|
log.error("Failed to batch save activation codes for batchId: {}", effectiveBatchId);
|
||||||
|
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "批量生成激活码失败");
|
||||||
|
}
|
||||||
|
log.info("Successfully generated {} activation codes with value {}, batchId: {}.", count, value, effectiveBatchId);
|
||||||
|
return generatedCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean redeemCode(Long currentUserId, String code) {
|
||||||
|
if (currentUserId == null || currentUserId <= 0) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的用户ID");
|
||||||
|
}
|
||||||
|
if (code == null || code.trim().isEmpty()) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "激活码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 查询激活码
|
||||||
|
QueryWrapper<ActivationCode> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("code", code.trim());
|
||||||
|
// is_delete = 0 会由 @TableLogic 自动处理
|
||||||
|
ActivationCode activationCode = this.getOne(queryWrapper);
|
||||||
|
|
||||||
|
if (activationCode == null) {
|
||||||
|
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "无效的激活码");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否已使用
|
||||||
|
if (activationCode.getIsUsed() == 1) {
|
||||||
|
throw new BusinessException(ErrorCode.OPERATION_ERROR, "此激活码已被使用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否过期
|
||||||
|
if (activationCode.getExpireTime() != null && LocalDateTime.now().isAfter(activationCode.getExpireTime())) {
|
||||||
|
throw new BusinessException(ErrorCode.OPERATION_ERROR, "此激活码已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 增加用户余额
|
||||||
|
boolean increaseSuccess = userService.increaseBalance(currentUserId, activationCode.getValue());
|
||||||
|
if (!increaseSuccess) {
|
||||||
|
// increaseBalance 内部应该会抛出异常如果失败(如用户不存在),或者返回false
|
||||||
|
log.error("Failed to increase balance for user {} while redeeming code {}", currentUserId, code);
|
||||||
|
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "余额充值失败,请稍后再试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 更新激活码状态
|
||||||
|
activationCode.setIsUsed(1);
|
||||||
|
activationCode.setUserId(currentUserId);
|
||||||
|
activationCode.setUseTime(LocalDateTime.now());
|
||||||
|
// updateTime 会自动更新
|
||||||
|
|
||||||
|
boolean updateResult = this.updateById(activationCode);
|
||||||
|
if (!updateResult) {
|
||||||
|
log.error("Failed to update activation code {} status after redeeming by user {}. Potential inconsistency!", code, currentUserId);
|
||||||
|
// 由于余额已增加,但激活码状态更新失败,这是一个危险状态。事务应回滚。
|
||||||
|
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新激活码状态失败,请联系客服");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("User {} successfully redeemed activation code {}. Value: {}", currentUserId, code, activationCode.getValue());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QueryWrapper<ActivationCode> getQueryWrapper(ActivationCodeQueryRequest activationCodeQueryRequest) {
|
||||||
|
QueryWrapper<ActivationCode> queryWrapper = new QueryWrapper<>();
|
||||||
|
if (activationCodeQueryRequest == null) {
|
||||||
|
return queryWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
String code = activationCodeQueryRequest.getCode();
|
||||||
|
Integer isUsed = activationCodeQueryRequest.getIsUsed();
|
||||||
|
String batchId = activationCodeQueryRequest.getBatchId();
|
||||||
|
Long userId = activationCodeQueryRequest.getUserId();
|
||||||
|
BigDecimal valueMin = activationCodeQueryRequest.getValueMin();
|
||||||
|
BigDecimal valueMax = activationCodeQueryRequest.getValueMax();
|
||||||
|
LocalDateTime expireTimeStart = activationCodeQueryRequest.getExpireTimeStart();
|
||||||
|
LocalDateTime expireTimeEnd = activationCodeQueryRequest.getExpireTimeEnd();
|
||||||
|
LocalDateTime createTimeStart = activationCodeQueryRequest.getCreateTimeStart();
|
||||||
|
LocalDateTime createTimeEnd = activationCodeQueryRequest.getCreateTimeEnd();
|
||||||
|
String sortField = activationCodeQueryRequest.getSortField();
|
||||||
|
String sortOrder = activationCodeQueryRequest.getSortOrder();
|
||||||
|
|
||||||
|
// 拼接查询条件
|
||||||
|
if (StringUtils.isNotBlank(code)) {
|
||||||
|
queryWrapper.like("code", code);
|
||||||
|
}
|
||||||
|
if (isUsed != null) {
|
||||||
|
queryWrapper.eq("isUsed", isUsed);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(batchId)) {
|
||||||
|
queryWrapper.eq("batchId", batchId);
|
||||||
|
}
|
||||||
|
if (userId != null && userId > 0) {
|
||||||
|
queryWrapper.eq("userId", userId);
|
||||||
|
}
|
||||||
|
if (valueMin != null) {
|
||||||
|
queryWrapper.ge("value", valueMin);
|
||||||
|
}
|
||||||
|
if (valueMax != null) {
|
||||||
|
queryWrapper.le("value", valueMax);
|
||||||
|
}
|
||||||
|
if (expireTimeStart != null) {
|
||||||
|
queryWrapper.ge("expireTime", expireTimeStart);
|
||||||
|
}
|
||||||
|
if (expireTimeEnd != null) {
|
||||||
|
queryWrapper.le("expireTime", expireTimeEnd);
|
||||||
|
}
|
||||||
|
if (createTimeStart != null) {
|
||||||
|
queryWrapper.ge("createTime", createTimeStart);
|
||||||
|
}
|
||||||
|
if (createTimeEnd != null) {
|
||||||
|
queryWrapper.le("createTime", createTimeEnd);
|
||||||
|
}
|
||||||
|
// 默认按创建时间降序
|
||||||
|
if (StringUtils.isNotBlank(sortField)) {
|
||||||
|
queryWrapper.orderBy(true, "ascend".equalsIgnoreCase(sortOrder), sortField);
|
||||||
|
} else {
|
||||||
|
queryWrapper.orderByDesc("create_time");
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public boolean deleteCode(Long codeId) {
|
||||||
|
if (codeId == null || codeId <= 0) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的激活码ID");
|
||||||
|
}
|
||||||
|
// 检查激活码是否存在,这步可选,因为removeById如果找不到也不会报错,但返回false
|
||||||
|
ActivationCode existingCode = this.getById(codeId);
|
||||||
|
if (existingCode == null || existingCode.getIsDelete() == 1) { // 已经是逻辑删除状态
|
||||||
|
// 可以选择抛出未找到错误,或者认为已经是"已删除"状态,返回true
|
||||||
|
// throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "激活码不存在或已被删除");
|
||||||
|
log.warn("Attempted to delete a non-existent or already deleted activation code with ID: {}", codeId);
|
||||||
|
return true; // 或者 false,取决于业务定义,这里认为已经是目标状态所以返回true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用MyBatis Plus的逻辑删除
|
||||||
|
boolean result = this.removeById(codeId);
|
||||||
|
if (!result) {
|
||||||
|
// 这种情况理论上不应该发生,除非并发删除了或者 getById 和 removeById 之间状态变了
|
||||||
|
log.error("Failed to logically delete activation code with ID: {}. removeById returned false.", codeId);
|
||||||
|
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除激活码失败");
|
||||||
|
}
|
||||||
|
log.info("Activation code with ID: {} logically deleted.", codeId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -140,25 +140,51 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
|||||||
public User getCurrentUser(HttpServletRequest request) {
|
public User getCurrentUser(HttpServletRequest request) {
|
||||||
// 优先从 SecurityContextHolder 获取认证信息
|
// 优先从 SecurityContextHolder 获取认证信息
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication != null && authentication.isAuthenticated() && !(authentication.getPrincipal() instanceof String && authentication.getPrincipal().equals("anonymousUser"))) {
|
if (authentication != null && authentication.isAuthenticated() && !(authentication.getPrincipal() instanceof String && "anonymousUser".equals(authentication.getPrincipal()))) {
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
if (principal instanceof User) {
|
if (principal instanceof User) {
|
||||||
return (User) principal; // principal 已经是 safetyUser
|
User userFromContext = (User) principal;
|
||||||
|
// 根据 Context 中的用户ID,从数据库重新获取最新的用户信息
|
||||||
|
User latestUser = this.getById(userFromContext.getId());
|
||||||
|
if (latestUser != null && latestUser.getIsDeleted() == 0) { // 确保用户未被删除
|
||||||
|
// 返回脱敏后的最新用户信息
|
||||||
|
return getSafetyUser(latestUser);
|
||||||
|
} else {
|
||||||
|
// 如果根据ID查不到用户了(例如被删除了),则认为未登录或异常
|
||||||
|
// 清除可能无效的认证信息
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户状态异常或已失效,请重新登录");
|
||||||
|
}
|
||||||
} else if (principal instanceof org.springframework.security.core.userdetails.User) {
|
} else if (principal instanceof org.springframework.security.core.userdetails.User) {
|
||||||
// 如果 principal 是 Spring Security 的 User (不太可能在这里,因为我们设置的是 safetyUser)
|
// 如果是Spring Security的User对象,通常包含username
|
||||||
// 需要转换或重新查询
|
org.springframework.security.core.userdetails.User springUser = (org.springframework.security.core.userdetails.User) principal;
|
||||||
// For now, assume it's our User object based on login logic
|
User userByUsername = this.getOne(new QueryWrapper<User>().eq("username", springUser.getUsername()));
|
||||||
|
if (userByUsername != null && userByUsername.getIsDeleted() == 0) {
|
||||||
|
return getSafetyUser(userByUsername);
|
||||||
|
} else {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户状态异常或已失效,请重新登录");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果 SecurityContextHolder 中没有,尝试从 session (旧逻辑,作为后备或移除)
|
// 如果 SecurityContextHolder 中没有有效信息,尝试从 session (旧逻辑,可作为后备)
|
||||||
|
// 注意:如果完全依赖 Spring Security,这部分可以考虑移除或调整
|
||||||
Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
|
Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
|
||||||
if (userObj instanceof User) {
|
if (userObj instanceof User) {
|
||||||
// 最好在这里也验证一下数据库中的用户状态,或者确保session中的信息足够可信
|
User userFromSession = (User) userObj;
|
||||||
return (User) userObj;
|
User latestUser = this.getById(userFromSession.getId());
|
||||||
|
if (latestUser != null && latestUser.getIsDeleted() == 0) {
|
||||||
|
// 如果session中的用户有效,也最好将其信息同步到SecurityContext以保持一致性 (可选)
|
||||||
|
// recreateAuthenticationInSecurityContext(latestUser, request); // 辅助方法,如果需要
|
||||||
|
return getSafetyUser(latestUser);
|
||||||
|
}
|
||||||
|
// Session 中的用户信息无效
|
||||||
|
request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE);
|
||||||
|
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "会话已过期或无效,请重新登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
|
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user