feat: 用户管理&订阅国企

This commit is contained in:
mozzie 2023-03-23 12:17:05 +08:00
parent aab88ebc0f
commit 50dfe31d32
12 changed files with 300 additions and 47 deletions

View File

@ -39,7 +39,7 @@ export const selectChapterList = ({
chapter_course_id: string; chapter_course_id: string;
}) => R.post("/api/course/chapter/select", { chapter_course_id }); }) => R.post("/api/course/chapter/select", { chapter_course_id });
export const updateChapter = (chapter: any) => export const updateChapter = (chapter: IChapter) =>
R.post("/api/course/chapter/update", chapter); R.post("/api/course/chapter/update", chapter);
export const removeCourse = (course: ICourseBasic) => export const removeCourse = (course: ICourseBasic) =>
@ -50,3 +50,7 @@ export const createChapter = (chapterList: IChapter[]) =>
export const removeChapter = (chapter: IChapter) => export const removeChapter = (chapter: IChapter) =>
R.post("/api/course/chapter/remove", chapter); R.post("/api/course/chapter/remove", chapter);
export const selectUserList = () => R.post("/api/user/admin/select/all");
export const updateUser = (user: any) => R.post("/api/user/admin/update", user);

View File

@ -1,5 +1,192 @@
import {
CheckOutlined,
EditOutlined,
SketchOutlined,
StopOutlined,
UserSwitchOutlined,
} from "@ant-design/icons";
import {
Button,
Card,
DatePicker,
Popconfirm,
Space,
Table,
Tooltip,
} from "antd";
import { RangePickerProps } from "antd/es/date-picker";
import dayjs from "dayjs";
import { useState } from "react";
import { selectUserList, updateUser } from "../../api";
import { useMount } from "../../hooks";
interface IEditUser {
key: string;
value: any;
}
const defaultEditUser: IEditUser = {
key: "",
value: "",
};
const User = () => { const User = () => {
return <div></div>; const [userList, setUserList] = useState([]);
const [editUser, setEditUser] = useState<IEditUser>(defaultEditUser);
const onConfirmEditUser = (user: any) => {
updateUser(user).then((res: any) => {
if (res?.code === 10000) renderUserList();
});
};
const disabledDate: RangePickerProps["disabledDate"] = (current) => {
return current && current < dayjs().endOf("day");
};
const columns = [
{
title: "id",
dataIndex: "id",
key: "id",
},
{
title: "用户",
dataIndex: "user_login",
key: "user_login",
},
{
title: "创建时间",
dataIndex: "user_create_time",
key: "user_create_time",
render: (_: any, record: any) => {
return (
<span>
{dayjs(+record.user_create_time).format("YYYY-MM-DD HH:mm:ss")}
</span>
);
},
},
{
title: "状态",
dataIndex: "user_status",
key: "user_status",
render: (_: any, record: any) => {
return record.user_status ? (
<CheckOutlined style={{ color: "#52c41a" }} />
) : (
"-"
);
},
},
{
title: "订阅",
dataIndex: "user_sub",
key: "user_sub",
render: (_: any, record: any) => {
return record.user_sub ? (
<CheckOutlined style={{ color: "#52c41a" }} />
) : (
"-"
);
},
},
{
title: "订阅到期",
dataIndex: "user_sub_expired",
key: "user_sub_expired",
render: (_: any, record: any) => {
return record.user_sub ? (
<Space>
<span>
{dayjs(+record.user_sub_expired).format("YYYY-MM-DD HH:mm:ss")}
</span>
<Popconfirm
placement="top"
title="订阅到期时间"
description={
<DatePicker
onChange={(date: any) => {
setEditUser({
key: "user_sub_expired",
value: "" + new Date(date).getTime(),
});
}}
presets={[
{ label: "订阅 - 年", value: dayjs().add(1, "year") },
{ label: "订阅 - 季度", value: dayjs().add(3, "month") },
{ label: "订阅 - 月", value: dayjs().add(1, "month") },
]}
disabledDate={disabledDate}
format="YYYY-MM-DD HH:mm:ss"
showTime={{ defaultValue: dayjs("00:00:00", "HH:mm:ss") }}
/>
}
onConfirm={() =>
onConfirmEditUser({ ...record, [editUser.key]: editUser.value })
}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<EditOutlined />} />
</Popconfirm>
</Space>
) : (
"-"
);
},
},
{
title: "操作",
dataIndex: "operation",
key: "operation",
render: (_: any, record: any) => {
return (
<Space>
<Tooltip title="封号">
<Button
type="dashed"
danger
icon={<StopOutlined />}
onClick={() =>
onConfirmEditUser({
...record,
user_status: !record.user_status,
})
}
/>
</Tooltip>
<Tooltip title="订阅">
<Button
type="default"
icon={<UserSwitchOutlined />}
onClick={() =>
onConfirmEditUser({ ...record, user_sub: !record.user_sub })
}
/>
</Tooltip>
</Space>
);
},
},
];
const renderUserList = () => {
selectUserList().then((res: any) => {
const { data = [] } = res;
setUserList(data.map((u: any) => ({ ...u, key: u.id })));
});
};
useMount(() => renderUserList());
return (
<div style={{ marginTop: 24 }}>
<Card>
<Table dataSource={userList} columns={columns} />
</Card>
</div>
);
}; };
export default User; export default User;

View File

@ -5,4 +5,5 @@ export enum BizCode {
OK = 10000, OK = 10000,
ERROR = 20000, ERROR = 20000,
AUTH = 40000, AUTH = 40000,
FORBID = 50000,
} }

View File

@ -69,7 +69,9 @@ export class CourseController {
const user = await this.userService.select({ user_login }); const user = await this.userService.select({ user_login });
// 用户订阅鉴权 // 用户订阅鉴权
if (!user.user_sub) if (!user.user_sub)
return { code: BizCode.AUTH, msg: '无权访问订阅课程' }; return { code: BizCode.FORBID, msg: '无权访问订阅课程' };
if (+user.user_sub_expired < Date.now())
return { code: BizCode.FORBID, msg: '订阅已过期' };
const course = await this.courseService.select({ course_id }); const course = await this.courseService.select({ course_id });
const chapterList = await this.chapterService.select(course_id); const chapterList = await this.chapterService.select(course_id);
const guide = await this.guideService.select(course_id); const guide = await this.guideService.select(course_id);

View File

@ -10,6 +10,7 @@ import { SmsDTO } from '../dto/sms.dto';
import { RedisService } from '@midwayjs/redis'; import { RedisService } from '@midwayjs/redis';
import * as CryptoJS from 'crypto-js'; import * as CryptoJS from 'crypto-js';
import { ADMIN, WEB } from '../config/base.config'; import { ADMIN, WEB } from '../config/base.config';
import { User } from '../entity/user.entity';
@Controller('/user') @Controller('/user')
export class UserController { export class UserController {
@Inject() @Inject()
@ -37,6 +38,9 @@ export class UserController {
if (!verifyCode) return { code: BizCode.ERROR, msg: '验证码无效' }; if (!verifyCode) return { code: BizCode.ERROR, msg: '验证码无效' };
// 查询用户是否存在 // 查询用户是否存在
const userExist = await this.userService.select(params); const userExist = await this.userService.select(params);
// 用户是否被封号
if (!userExist?.user_status)
return { code: BizCode.FORBID, msg: '您的账号被封禁' };
const payload = userExist?.id const payload = userExist?.id
? userExist ? userExist
: await this.userService.createUser(params); : await this.userService.createUser(params);
@ -96,6 +100,11 @@ export class UserController {
try { try {
const token = this.ctx.cookies.get(WEB.SIGN); const token = this.ctx.cookies.get(WEB.SIGN);
const user = decodeToken(token); const user = decodeToken(token);
const { user_login } = user;
// 查询用户是否被封号
const user_current = await this.userService.select({ user_login });
const { user_status } = user_current;
if (!user_status) return { code: BizCode.FORBID, msg: '您的账号被封禁' };
return { code: BizCode.OK, data: user }; return { code: BizCode.OK, data: user };
} catch (error) { } catch (error) {
this.ctx.logger.error(error); this.ctx.logger.error(error);
@ -107,17 +116,39 @@ export class UserController {
async verifyCode(@Body() params: SmsDTO) { async verifyCode(@Body() params: SmsDTO) {
try { try {
const { phoneNumber: phoneNumbers, sign } = params; const { phoneNumber: phoneNumbers, sign } = params;
// 查询手机号是否被封禁
const user = await this.userService.select({ user_login: phoneNumbers });
if (user && !user.user_status)
return { code: BizCode.FORBID, msg: '您的账号被封禁' };
// 防止接口调用 start
const decrypted = CryptoJS.AES.decrypt(sign, phoneNumbers); const decrypted = CryptoJS.AES.decrypt(sign, phoneNumbers);
const hackAction = decrypted.toString(CryptoJS.enc.Utf8) !== phoneNumbers; const hackAction = decrypted.toString(CryptoJS.enc.Utf8) !== phoneNumbers;
if (hackAction) return { code: BizCode.ERROR, msg: 'fuck u' }; if (hackAction) return { code: BizCode.ERROR, msg: 'fuck u' };
// 防止接口调用 end
const code = Math.floor(Math.random() * 9000 + 1000); const code = Math.floor(Math.random() * 9000 + 1000);
await this.redisService.set('' + phoneNumbers, code, 'EX', 60); await this.redisService.set('' + phoneNumbers, code, 'EX', 60);
await this.smsService.send({ code, phoneNumbers }); await this.smsService.send({ code, phoneNumbers });
return { code: BizCode.OK }; return { code: BizCode.OK };
} catch (error) { } catch (error) {
console.log(error);
this.ctx.logger.error(error); this.ctx.logger.error(error);
return { code: BizCode.ERROR, msg: '[error] /web/sms error' }; return { code: BizCode.ERROR, msg: '[error] /web/sms error' };
} }
} }
@Post('/admin/select/all')
async selectUser() {
const data = await this.userService.selectAll();
return { code: BizCode.OK, data };
}
@Post('/admin/update')
async updateUser(@Body() user: User) {
try {
await this.userService.update(user);
return { code: BizCode.OK };
} catch (error) {
this.ctx.logger.error(error);
return { code: BizCode.ERROR, msg: error };
}
}
} }

View File

@ -25,4 +25,7 @@ export class User {
@Column({ default: false }) @Column({ default: false })
user_sub?: boolean; user_sub?: boolean;
@Column()
user_sub_expired?: string;
} }

View File

@ -17,6 +17,10 @@ export class UserService {
return user; return user;
} }
async selectAll(): Promise<User[]> {
return await this.userModel.find();
}
async createUser(user: User) { async createUser(user: User) {
const h = hash('' + user.user_login); const h = hash('' + user.user_login);
user.display_name = h.substring(0, 8); user.display_name = h.substring(0, 8);
@ -24,4 +28,8 @@ export class UserService {
const result = await this.userModel.save(user); const result = await this.userModel.save(user);
return result; return result;
} }
async update(user: User) {
this.userModel.save(user);
}
} }

View File

@ -1,5 +1,7 @@
import axios from "axios"; import axios from "axios";
import Cookies from "js-cookie";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { removeLoginCookies } from "../util/cookie";
const config = { const config = {
baseURL: "", baseURL: "",
@ -38,6 +40,10 @@ instance.interceptors.response.use(
case 40000: case 40000:
toast.error(msg); toast.error(msg);
break; break;
case 50000:
toast.error(msg);
removeLoginCookies();
break;
default: default:
// TODO ... // TODO ...
break; break;

View File

@ -1,8 +1,8 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import Cookies from "js-cookie";
import { useUserStore } from "../store/user.store"; import { useUserStore } from "../store/user.store";
import Result from "../components/Result"; import Result from "../components/Result";
import { withLoginCookies } from "../util/cookie";
interface IGuardProps { interface IGuardProps {
children: JSX.Element; children: JSX.Element;
@ -16,14 +16,13 @@ export const Guard = (props: IGuardProps) => {
const fetchUser = useUserStore((s: any) => s.fetchUser); const fetchUser = useUserStore((s: any) => s.fetchUser);
const location = useLocation(); const location = useLocation();
const sign = Cookies.get("_sign_web");
const needAuth = needAuthList.some((p) => location.pathname.indexOf(p) > -1); const needAuth = needAuthList.some((p) => location.pathname.indexOf(p) > -1);
useEffect(() => { useEffect(() => {
if (!user) fetchUser(); if (!user) fetchUser();
}, [location.pathname]); }, [location.pathname]);
if (!sign && needAuth) return <Result code={405} />; if (!withLoginCookies() && needAuth) return <Result code={405} />;
return props.children; return props.children;
}; };

View File

@ -1,15 +1,14 @@
import { create } from "zustand"; import { create } from "zustand";
import Cookie from "js-cookie";
import { userState } from "../api"; import { userState } from "../api";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { removeLoginCookies, withLoginCookies } from "../util/cookie";
export const useUserStore = create((set) => { export const useUserStore = create((set) => {
return { return {
user: null, user: null,
setUser: (user: any) => set({ user }), setUser: (user: any) => set({ user }),
fetchUser: async () => { fetchUser: async () => {
const sign = Cookie.get("_sign_web"); if (!withLoginCookies()) return set({ user: null });
if (!sign) return set({ user: null });
userState().then((res: any) => { userState().then((res: any) => {
const { code, data } = res; const { code, data } = res;
if (code === 10000) set({ user: data }); if (code === 10000) set({ user: data });
@ -17,8 +16,7 @@ export const useUserStore = create((set) => {
}, },
userExit: () => { userExit: () => {
set({ user: null }); set({ user: null });
Cookie.remove("_sign_web"); removeLoginCookies();
Cookie.remove("_sign_web.sig");
toast.success("再见~!"); toast.success("再见~!");
}, },
}; };

View File

@ -0,0 +1,10 @@
import Cookie from "js-cookie";
export const withLoginCookies = () => {
return Cookie.get("_sign_web") && Cookie.get("_sign_web.sig");
};
export const removeLoginCookies = () => {
Cookie.remove("_sign_web");
Cookie.remove("_sign_web.sig");
};

View File

@ -80,7 +80,8 @@ function CourseDetail() {
return ( return (
<div className="course-detail"> <div className="course-detail">
{toc.length > 0 && ( {toc.length > 0 ? (
<>
<aside className="table-of-content"> <aside className="table-of-content">
<h2>{course.course_title}</h2> <h2>{course.course_title}</h2>
<div> <div>
@ -114,10 +115,13 @@ function CourseDetail() {
})} })}
</div> </div>
</aside> </aside>
)} <div className="content" style={{ left: 300 }}>
<div className="content" style={{ left: toc.length > 0 ? "300px" : 0 }}>
{view} {view}
</div> </div>
</>
) : (
<Result code={403} />
)}
</div> </div>
); );
} }