master
江一阳 2 years ago
parent 3d60e8d047
commit e996b958ec

@ -1 +0,0 @@
ECHO 处于打开状态。

@ -1 +0,0 @@
undefined

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

@ -1,18 +0,0 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support For `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

@ -1,9 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
}

@ -1,24 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
Brand: typeof import('./src/components/brand/brand.vue')['default']
CommonButton: typeof import('./src/components/button/commonButton.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
Header: typeof import('./src/components/header/header.vue')['default']
IconInput: typeof import('./src/components/input/iconInput.vue')['default']
LeftMenu: typeof import('./src/components/menu/leftMenu.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

@ -1,17 +0,0 @@
interface IResponseData<T> {
code: number
message: string
data: T
}
interface ILoginRequestData {
input_username: string
input_password: string
verfiyCode: string
}
interface ILoginResponseData {
username: string
token: string
token_expire: number
}

@ -1,92 +0,0 @@
import { MockMethod } from 'vite-plugin-mock'
import { Random } from 'mockjs'
// code 0 success
// code 505 login error
interface IOrder {
id: string;
createAt: string;
updateAt: string;
college: string;
pm_name: string;
pm_tel: number;
order_time: string;
size: number;
need: boolean;
status: number;
verify_url: string;
sub_pm_type: string;
}
const order_list: IOrder[] = [];
// x 100
for (let i = 0; i < 100; i++) {
order_list.push({
college: Random.csentence(5, 10),
createAt: Random.datetime(),
id: Random.guid().slice(0, 10),
pm_name: Random.cname(),
pm_tel: Random.integer(10000000000, 19999999999),
size: Random.integer(1, 100),
need: Random.boolean(),
order_time: Random.datetime(),
status: Random.integer(0, 3),
updateAt: Random.datetime(),
verify_url: Random.url(),
// IMG or PDF
sub_pm_type: Random.pick(['IMG', 'PDF'])
})
}
export default [
// user login
{
url: '/api/user/login',
method: 'post',
// res
response: (data) => {
const { input_username, input_password, verfiyCode } = data.body;
if (input_username === 'admin' &&
input_password === 'admin123' &&
verfiyCode === '1234') {
return {
code: 0,
message: 'success',
data: {
token: 'Token',
username: 'user_1',
token_expire: 3600 * 24,
}
}
} else {
// test error
return {
code: 505,
message: '用户名或密码错误',
data: {}
}
}
}
}
// 请求订单假数据
,
{
url: '/api/order/list',
method: 'get',
// res
response: (data) => {
console.log(data.query);
const { page, limit } = data.query;
// data 传入分页数据
const query_order_list: IOrder[] = [];
// 根据page limit 从order_list截取 返回数据
const start = (page - 1) * limit;
const end = page * limit;
query_order_list.push(...order_list.slice(start, end));
return query_order_list;
}
}
] as MockMethod[]

@ -1,34 +0,0 @@
{
"name": "cauc_admin",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@types/mockjs": "^1.0.7",
"axios": "^1.5.0",
"element-plus": "^2.3.14",
"fast-glob": "^3.3.1",
"mitt": "^3.0.1",
"pinia": "^2.1.6",
"remixicon": "^3.5.0",
"sass": "^1.68.0",
"vite-plugin-svg-icons": "^2.0.1",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@rollup/plugin-image": "^3.0.2",
"@vitejs/plugin-vue": "^4.2.3",
"mockjs": "^1.1.0",
"typescript": "^5.0.2",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.4.5",
"vite-plugin-mock": "^2.9.8",
"vue-tsc": "^1.8.5"
}
}

File diff suppressed because it is too large Load Diff

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@ -1,9 +0,0 @@
<template>
<div>
<RouterView />
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped></style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

@ -1,53 +0,0 @@
<template>
<div
class="container"
:style="{
width: _bg_size + 'vh',
height: _bg_size + 'vh',
backgroundSize: `${_size}vh,${_size}vh`,
}"
></div>
</template>
<script setup lang="ts">
import { PropType, onMounted } from "vue";
import { ref } from "vue";
const enum _Size {
small = 0.4,
normal = 1,
large = 1.5,
}
const normal_bg_size = 11;
const normal_size = 9;
const props = defineProps({
size: {
type: String as PropType<"small" | "normal" | "large">,
},
});
const _scale = ref(1);
const _size = ref();
const _bg_size = ref();
onMounted(() => {
_scale.value =
props.size === "large"
? _Size.large
: props.size === "small"
? _Size.small
: _Size.normal;
_size.value = normal_size * _scale.value;
_bg_size.value = normal_bg_size * _scale.value;
});
</script>
<style lang="scss" scoped>
.container {
background-image: url("../../assets/images/icon/brand/leaf_top.png"),
url("../../assets/images/icon/brand/leaf_bot.png");
background-repeat: no-repeat;
background-blend-mode: luminosity;
background-position: 0 0, right bottom;
}
</style>

@ -1,29 +0,0 @@
<template>
<button type="button" class="common-button" @click="click">{{ text }}</button>
</template>
<script setup lang="ts">
const props = defineProps({
text: {
type: String,
default: "button",
},
click: {
type: Function,
default: () => {},
},
});
</script>
<style lang="scss" scoped>
.common-button {
width: 100%;
height: 40px;
border-radius: var(--common-radius);
background-color: #409eff;
color: #fff;
border: none;
outline: none;
cursor: pointer;
}
</style>

@ -1,27 +0,0 @@
<template>
<div class="header-area">
<i class="ri-list-unordered" @click="toggle_menu"></i>
<div class="header-group"></div>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance } from "vue";
const ctx = getCurrentInstance();
const bus = ctx?.appContext.config.globalProperties.$bus;
const toggle_menu = () => {
bus?.emit("toggle_menu");
};
</script>
<style lang="scss" scoped>
.header-area {
background-color: var(--header-bg-color);
height: var(--menu-brand-height);
display: flex;
align-items: center;
padding: 0 var(--common-padding);
background-color: var(--content-bg-color);
}
</style>

@ -1,142 +0,0 @@
<template>
<div
class="icon-input-content"
:class="{
focus: input_focus,
}"
:style="{
width: props.width,
}"
>
<i :class="left_icon" class="icon-1"></i>
<input
class="input"
:placeholder="placeholder"
v-model="val"
:type="type === 'password' ? (show_pwd ? 'text' : 'password') : type"
@focus="on_focus"
@blur="on_blur"
@input="on_input"
:name="type === 'password' ? 'leaf-admin-password' : 'leaf-admin-text'"
autocomplete="leaf-admin-password"
/>
<Transition name="fade">
<i
v-show="val"
class="ri-close-circle-line"
:class="{
'icon-2': type === 'password',
'icon-3': type !== 'password',
}"
@click="clear"
></i>
</Transition>
<i
v-show="type === 'password'"
class="ri-eye-line icon-3"
:class="{
'ri-eye-fill': show_pwd,
'ri-eye-off-line': !show_pwd,
}"
@click="toggle_show_pwd"
></i>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const props = defineProps({
type: {
type: String,
default: "text",
},
width: {
type: String,
default: "100%",
},
placeholder: {
type: String,
},
modelValue: {
type: String,
default: "",
},
left_icon: {
type: String,
default: "",
},
});
const input_focus = ref(false);
const val = ref(props.modelValue);
// pwd
const show_pwd = ref(false);
const emit = defineEmits(["update:modelValue"]);
const on_input = () => {
emit("update:modelValue", val.value);
};
const on_focus = () => {
input_focus.value = true;
};
const on_blur = () => {
console.log("blur");
input_focus.value = false;
};
const clear = () => {
val.value = "";
};
const toggle_show_pwd = () => {
show_pwd.value = !show_pwd.value;
};
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.icon-input-content {
border: 1.5px solid #e5e5e5;
border-radius: var(--common-radius);
padding-block: 1.2vh;
display: grid;
grid-template-columns: 40px 1fr 20px 40px;
grid-template-areas: "icon-1 input icon-2 icon-3";
justify-items: center;
align-items: center;
transition: all 0.3s;
height: max-content;
&.focus {
border-color: var(--common-input-focus-color);
transition: all 0.3s;
}
input {
border: none;
inset: none;
width: 100%;
height: max-content;
&:focus {
outline: none;
}
grid-area: "input";
}
.icon-1 {
grid-area: icon-1;
}
.icon-2 {
grid-area: icon-2;
}
.icon-3 {
grid-area: icon-3;
}
}
</style>

@ -1,142 +0,0 @@
<template>
<div
class="menu-area"
:class="{
active: only_show_icon,
}"
v-if="show_menu"
>
<div class="brand">
<div class="icon">
<Brand size="small"></Brand>
</div>
<span>LeafAdmin</span>
</div>
<div
class="menu-item"
@click="linkTo(menuItem.link_name)"
v-for="(menuItem, idx) in menu_list"
>
<i class="icon" :class="menuItem.icon"></i>
<div class="menu-item-name">{{ menuItem.name }}</div>
</div>
<div class="menu-footer menu-item">
<i class="ri-skip-back-fill icon" @click="toggle_menu"></i>
</div>
</div>
</template>
<script setup lang="ts">
import Brand from "@/components/brand/brand.vue";
import { ref, getCurrentInstance, onMounted } from "vue";
import { router } from "@/router";
const bus = getCurrentInstance()?.appContext.config.globalProperties.$bus;
const show_menu = ref(true);
const only_show_icon = ref(false);
const menu_list = [
{
name: "首页",
icon: "ri-dashboard-fill",
path: "/dashboard",
link_name: "Home",
},
{
name: "订单管理",
icon: "ri-bookmark-3-line",
path: "/order",
link_name: "Order",
},
{
name: "用户管理",
icon: "ri-folder-user-line",
path: "/setting",
link_name: "User",
},
{
name: "其他",
icon: "ri-psychotherapy-line",
path: "/other",
link_name: "Other",
},
];
onMounted(() => {
bus?.on("toggle_menu", () => {
show_menu.value = !show_menu.value;
});
});
const toggle_menu = () => {
only_show_icon.value = !only_show_icon.value;
};
const linkTo = (name: string) => {
router.push({
name: name,
});
};
</script>
<style lang="scss" scoped>
.menu-area {
width: 200px;
height: 100%;
background-color: var(--menu-bg-color);
position: relative;
transition: var(--common-transition);
overflow-x: hidden;
&.active {
width: var(--menu-icon-size);
transition: var(--common-transition);
}
.brand {
background-color: var(--menu-brand-bg-color) !important;
color: var(--menu-brand-color);
font-weight: 600;
font-size: 20px !important;
height: var(--menu-brand-height);
display: grid;
grid-template-columns: var(--menu-icon-size) 1fr;
gap: 1vw;
align-items: center;
color: var(--menu-brand-color);
font-size: 15px;
transition: var(--common-transition);
.icon {
justify-self: center;
}
}
.menu-item {
// border: 1px solid blue;
height: var(--menu-item-height);
display: grid;
grid-template-columns: var(--menu-icon-size) 1fr;
gap: 1vw;
align-items: center;
color: var(--menu-item-color);
font-size: 15px;
transition: var(--common-transition);
white-space: nowrap;
.icon {
justify-self: center;
}
&:hover {
filter: brightness(1.5);
transition: var(--common-transition);
}
.menu-item-name {
justify-self: start;
}
}
.menu-footer {
position: absolute;
bottom: 0;
width: 100%;
height: var(--menu-item-height);
background-color: var(--menu-footer-bg-color);
}
}
</style>

@ -1,38 +0,0 @@
<template>
<div class="common-layout-area">
<div class="menu-area">
<LeftMenu></LeftMenu>
</div>
<div class="content">
<Header></Header>
<div class="main-content">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import LeftMenu from "@/components/menu/leftMenu.vue";
import Header from "@/components/header/header.vue";
</script>
<style lang="scss" scoped>
.common-layout-area {
display: grid;
grid-template-columns: max-content 1fr;
height: 100vh;
max-height: 100vh;
.content {
background-color: var(--content-bg-bt-color);
height: 100%;
display: grid;
grid-template-rows: max-content 1fr;
// border: 1px solid red;
.main-content {
padding: var(--common-padding);
overflow: hidden;
}
}
}
</style>

@ -1,23 +0,0 @@
import { createApp } from 'vue'
import '@/style.css'
import 'element-plus/dist/index.css'
import 'remixicon/fonts/remixicon.css'
import App from './App.vue'
import setupRouter from './router'
import pinia from '@/store'
import mitt from 'mitt'
const bootstrap = () => {
const app = createApp(App)
setupRouter(app)
app.use(pinia)
const bus = mitt()
app.config.globalProperties.$bus = bus
app.mount('#app')
}
bootstrap()

@ -1,17 +0,0 @@
import { createRouter, createWebHistory, Router } from 'vue-router'
import { constantRoutes as routes } from '@/router/modules'
import { App } from 'vue';
import guard from '@/router/modules/gurad'
export const router: Router = createRouter({
history: createWebHistory(),
routes: routes,
});
const setupRouter = (app: App) => {
guard(router)
app.use(router)
}
export default setupRouter;

@ -1,68 +0,0 @@
import { Router, RouteLocationNormalized } from 'vue-router'
import { useUserStoreForSetup } from '@/store/user'
import { storeToRefs } from 'pinia'
import { store, load, remove } from '@/utils/json'
const enum LogintorageKey {
USER_TOKEN_KEY = 'user_token_key'
}
class Gurad {
constructor(
private router: Router,
private gurad_token_expire: number = 0
) { }
public run() {
this.router.beforeEach((to, from) => this.beforeEach(to, from))
}
private beforeEach(to: RouteLocationNormalized, from: RouteLocationNormalized) {
if (to.meta.auth && !this.authToken()) {
return 'login'
}
}
private authToken() {
const now = Math.floor(Date.now() / 1000)
if (!isNaN(this.gurad_token_expire) && this.gurad_token_expire > now) {
console.log(this.gurad_token_expire);
return true;
}
// 检查浏览器中是否有缓存
const storage: IStoreData<ILoginStorage> | null = load<ILoginStorage>(LogintorageKey.USER_TOKEN_KEY)
if (storage) {
// 检查是否过期
if (storage.data.token_expire > now) {
// 未过期
this.gurad_token_expire = storage.data.token_expire
return true
}
remove(LogintorageKey.USER_TOKEN_KEY)
}
this.gurad_token_expire = 0;
const userStore = useUserStoreForSetup()
const { token, token_expire } = storeToRefs(userStore)
// 获取当前时间戳
if (!token_expire.value || token_expire.value < now) {
return false
}
this.gurad_token_expire = token_expire.value
store<ILoginStorage>(LogintorageKey.USER_TOKEN_KEY, {
data: {
token: token.value,
token_expire: token_expire.value
}
})
return true
}
}
export default (router: Router) => {
new Gurad(router).run()
}

@ -1 +0,0 @@
export * from '@/router/modules/routes'

@ -1,64 +0,0 @@
import { RouteRecordRaw } from 'vue-router'
export const constantRoutes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
meta: {
auth: true
},
component: () => import('@/views/home/home.vue'),
children: [
{
path: '/',
name: 'Dashboard',
meta: {
auth: true
},
component: () => import('@/views/dashboard/dashboard.vue')
},
{
path: '/order',
name: 'Order',
meta: {
auth: true
},
component: () => import('@/views/order/order.vue')
},
{
path: '/user',
name: 'User',
meta: {
auth: true
},
component: () => import('@/views/user/user.vue')
},
{
path: '/other',
name: 'Other',
meta: {
auth: true
},
component: () => import('@/views/other/other.vue')
}
]
},
{
path: '/login',
name: 'Login',
meta: {
auth: false
},
component: () => import('@/views/login/login.vue')
},
{
path: '/test',
name: 'Test',
meta: {
auth: false
},
component: () => import('@/views/test/test.vue')
}
]

@ -1,18 +0,0 @@
import { defineStore } from 'pinia'
export const useCountStoreForSetup = defineStore('count', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
},
getters: {
doubleCount(): number {
return this.count * 2
}
}
})

@ -1,5 +0,0 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia

@ -1,60 +0,0 @@
import axios from 'axios'
import { defineStore } from 'pinia'
import { computed, ref, h } from 'vue'
import { ElMessage } from 'element-plus'
export const useUserStoreForSetup = defineStore('user', () => {
//state
// 用户名
const username = ref('')
// 登陆token
const token = ref('')
// token过期时间
const token_expire = ref(0)
//getters
// function
const login = async (userData: ILoginRequestData) => {
// axios
const res: IResponseData<ILoginResponseData> = await axios.post('/api/user/login', userData
).then(res => res.data)
if (res.code === 0) {
username.value = res.data.username
token.value = res.data.token
// 获得当前时间戳
const now = Math.floor(Date.now() / 1000)
token_expire.value = now + res.data.token_expire
ElMessage({
message: '登陆成功',
type: 'success',
duration: 2000
})
return true
} else {
ElMessage({
message: res.message,
type: 'error',
duration: 2000
})
return false
}
}
const logout = () => {
username.value = '';
token.value = ''
}
return {
username,
token,
token_expire,
login,
logout
}
})

@ -1,45 +0,0 @@
:root {
/* color */
--common-color: #386bf3;
--bg-primary-color: #fefefe;
--bg-left-container-color: #386bf3;
--common-icon-color: #a1a1a1;
--common-input-focus-color: #386bf3;
--menu-bg-color: #001529;
--menu-brand-bg-color: #002140;
--menu-brand-color: #fefefe;
--menu-item-color: #aaa;
--menu-footer-bg-color: #002140;
--content-bg-color: #ffffff;
--content-bg-bt-color: #f0f2f5;
/* radius */
--common-radius: 4px;
/* transition */
--common-transition: all 0.3s;
/* h */
--menu-item-height: 6vh;
--menu-brand-height: 7vh;
/* width */
--menu-icon-size: 60px;
/* --padding */
--common-padding: 1vw;
--common-padding-2: 2vw;
/* button */
--button-primary-bg-color: #386bf3;
--button-primary-color: var(--bg-primary-color);
--button-warning-bg-color: #ff7300;
--button-warning-color: var(--bg-primary-color);
}
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
input[type="password"]::-ms-reveal {
display: none
}

@ -1,17 +0,0 @@
interface IResponseData<T> {
code: number
message: string
data: T
}
interface ILoginRequestData {
input_username: string
input_password: string
verfiyCode: string
}
interface ILoginResponseData {
username: string
token: string
token_expire: number
}

@ -1,5 +0,0 @@
declare module "*.vue" {
import { DefineComponent } from "vue"
const component: DefineComponent<{}, {}, any>
export default component
}

@ -1,9 +0,0 @@
interface IStoreData<T> {
data: T
}
interface ILoginStorage {
token: string
token_expire: number
}

@ -1,14 +0,0 @@
interface IOrder {
id: string;
createAt: string;
updateAt: string;
college: string;
pm_name: string;
pm_tel: number;
size: number;
order_time:string;
need:boolean;
status: number;
verify_url: string;
sub_pm_type: string;
}

@ -1,7 +0,0 @@
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
auth?: boolean;
}
}

@ -1,5 +0,0 @@
export const enum _Size_type {
small = "small",
normal = "normal",
large = "large",
}

@ -1,22 +0,0 @@
// 序列化
const store = <T>(key: string, item: IStoreData<T>) => {
const data_json = JSON.stringify(item)
// 存储在浏览器中
localStorage.setItem(key, data_json)
}
// 反序列化
const load = <T>(name: string): IStoreData<T> | null => {
const data_json = localStorage.getItem(name)
if (data_json) {
return JSON.parse(data_json)
}
return null
}
//删除
const remove = (name: string) => {
localStorage.removeItem(name)
}
export { store, load, remove }

@ -1,3 +0,0 @@
export const getAssetsImage = (name: string) => {
return new URL(`/src/assets/${name}`, import.meta.url).href
}

@ -1,13 +0,0 @@
<template>
<div>
darshboard
</div>
</template>
<script setup lang="ts">
</script>
<style lang='scss' scoped>
</style>

@ -1,9 +0,0 @@
<template>
<CommonLayout></CommonLayout>
</template>
<script setup lang="ts">
import CommonLayout from "@/layouts/common/commonLayout.vue";
</script>
<style lang="scss" scoped></style>

@ -1,171 +0,0 @@
<template>
<div class="main-content">
<div class="left-content">
<div class="bgitem"></div>
<div class="bgitem"></div>
<div class="bgitem"></div>
<div class="bgitem"></div>
<img
class="left-cover"
:src="getAssetsImage('svg/login/cover_0.svg')"
alt=""
/>
</div>
<div class="right-content">
<div class="top-area">
<Brand />
<div class="login-title">LEAF ADMIN</div>
</div>
<form class="login-form">
<IconInput
left_icon="ri-user-3-fill"
v-model="input_username"
placeholder="username"
></IconInput>
<IconInput
left_icon="ri-key-fill"
v-model="input_password"
placeholder="password"
type="password"
></IconInput>
<IconInput
left_icon="ri-shield-cross-line"
v-model="input_code"
placeholder="verification"
type="text"
></IconInput>
<div class="remember-me">
<input
type="checkbox"
id="custom-checkbox"
:checked="true"
label="Remember me"
/>
<label for="custom-checkbox">Remember Me</label>
</div>
<CommonButton text="Login" :click="login"></CommonButton>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { getAssetsImage } from "@/utils/static/pub-use";
import Brand from "@/components/brand/brand.vue";
import IconInput from "@/components/input/iconInput.vue";
import CommonButton from "@/components/button/commonButton.vue";
import { ref } from "vue";
import { useUserStoreForSetup } from "@/store/user";
import { storeToRefs } from "pinia";
import { router } from "@/router";
const input_username = ref("admin");
const input_password = ref("admin123");
const input_code = ref("1234");
const a = 1;
const userStore = useUserStoreForSetup();
const { username, token, token_expire } = storeToRefs(userStore);
const login = async () => {
const is_login = await userStore.login({
input_username: input_username.value,
input_password: input_password.value,
verfiyCode: input_code.value,
});
if (is_login) {
router.push({ name: "Home" });
}
};
</script>
<style lang="scss" scoped>
.main-content {
height: 100vh;
display: grid;
grid-template-columns: 1fr 1fr;
background-color: var(--bg-primary-color);
.left-content {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
position: relative;
// 13
.bgitem:nth-child(1),
.bgitem:nth-child(3) {
background-color: var(--bg-left-container-color);
}
.bgitem:nth-child(1) {
position: relative;
&::before {
position: absolute;
content: "";
width: 100%;
height: 100%;
background-color: var(--bg-primary-color);
border-bottom-left-radius: 100%;
border-top-left-radius: none;
}
}
.bgitem:nth-child(4) {
position: relative;
&::before {
position: absolute;
content: "";
width: 100%;
height: 100%;
background-color: var(--bg-left-container-color);
border-top-right-radius: 100%;
border-bottom-right-radius: none;
}
}
.left-cover {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40%;
height: 40%;
object-fit: cover;
}
}
@media screen and (max-width: 1024px) {
& {
grid-template-columns: 1fr;
}
.left-content {
display: none;
}
}
.right-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2vh;
.top-area {
display: flex;
align-items: center;
}
.login-form {
height: max-content;
border-radius: var(--common-radius);
min-width: 300px;
width: 50%;
border: 1.5px solid var(--common-color);
padding: var(--common-padding-2);
display: flex;
flex-direction: column;
gap: 2vh;
.remember-me {
display: flex;
align-items: center;
input {
margin-right: 1vw;
}
}
}
}
}
</style>

@ -1,202 +0,0 @@
<template>
<div class="order-area">
<el-table :data="tableData" stripe style="width: 100%" class="center-table">
<el-table-column
type="index"
label="序号"
width="80"
fixed="left"
:align="'center'"
>
<template v-slot="{ $index }">
<span>{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="id" label="订单ID" width="120" :align="'center'" />
<el-table-column
prop="college"
label="所属学院"
width="180"
:align="'center'"
/>
<el-table-column
prop="pm_name"
label="授权人名称"
width="100"
:align="'center'"
/>
<el-table-column
prop="pm_tel"
label="授权人电话"
width="120"
:align="'center'"
/>
<el-table-column
sortable
prop="status"
label="订单状态"
:align="'center'"
>
<template v-slot="{ row }">
<div>
{{
row.status === 0
? "未审核"
: row.status === 1
? "已审核"
: "已取消"
}}
</div>
</template>
</el-table-column>
<el-table-column
prop="verify_url"
label="授权书链接"
width="120"
show-overflow-tooltip
:align="'center'"
>
<template v-slot="{ row }">
<div>
<button @click="() => getPermission(row.verify_url)">
查看授权书
</button>
</div>
</template>
</el-table-column>
<el-table-column
sortable
prop="order_time"
label="订单创建时间"
width="180"
:align="'center'"
/>
<el-table-column
sortable
prop="size"
label="预定参观人数"
width="120"
:align="'center'"
/>
<el-table-column prop="need" label="指导员需求"
width="120"
:align="'center'" />
<el-table-column
prop="sub_pm_type"
label="授权书格式"
:align="'center'"
width="120"
/>
<el-table-column
sortable
prop="createAt"
label="创建时间"
width="180"
:align="'center'"
/>
<el-table-column
prop="updateAt"
label="更新时间"
width="180"
:align="'center'"
/>
<!-- ops groups -->
<el-table-column
label="操作"
fixed="right"
width="150"
show-overflow-tooltip
:align="'center'"
>
<template v-slot="{ row }">
<div class="ops-group">
<i class="ri-edit-box-line op-icon primary"></i>
<i class="ri-delete-bin-2-line op-icon warning"></i>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import axios from "axios";
import { onMounted, ref } from "vue";
const row_height = 50;
const limit_row = 10;
const tableData = ref([]);
onMounted(() => {
axios
.get("/api/order/list", {
params: {
limit: limit_row,
page: 2,
},
})
.then((res) => {
tableData.value = res.data;
});
axios.get("http://localhost:3000/api/order/all").then((res) => {
console.log(res.data);
});
});
const getPermission = (url: string) => {
console.log(url);
};
</script>
<style lang="scss" scoped>
.ops-group {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
.op-icon {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
&.primary {
border: 1.5px solid var(--button-primary-bg-color);
color: var(--button-primary-bg-color);
transition: var(--common-transition);
&:hover {
background-color: var(--button-primary-bg-color);
color: var(--button-primary-color);
transition: var(--common-transition);
}
}
&.warning {
border: 1.5px solid var(--button-warning-bg-color);
color: var(--button-warning-bg-color);
transition: var(--common-transition);
&:hover {
background-color: var(--button-warning-bg-color);
color: var(--button-warning-color);
transition: var(--common-transition);
}
}
}
}
.order-area {
height: 100%;
background-color: var(--content-bg-color);
padding: var(--common-padding);
.test {
height: 100%;
position: relative;
}
}
</style>

@ -1,13 +0,0 @@
<template>
<div>
other
</div>
</template>
<script setup lang='ts'>
</script>
<style lang='scss' scoped>
</style>

@ -1,77 +0,0 @@
<template>
<el-table
:data="tableData"
stripe
style="width: 100%"
:height="250"
:row-style="{
height: `${row_height}px`,
}"
>
<el-table-column type="index" label="序号" width="80">
<template v-slot="{ $index }">
<span>{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="id" label="订单ID" width="180" />
<el-table-column sortable prop="createAt" label="创建时间" width="180" />
<el-table-column prop="updateAt" label="更新时间" width="180" />
<el-table-column prop="college" label="所属学院" width="180" />
<el-table-column prop="pm_name" label="授权人名称" width="180" />
<el-table-column prop="pm_tel" label="授权人电话" width="180" />
<el-table-column
sortable
prop="order_time"
label="订单创建时间"
width="180"
/>
<el-table-column sortable prop="size" label="预定参观人数" />
<el-table-column prop="need" label="指导员需求" />
<el-table-column sortable prop="status" label="订单状态" />
<el-table-column prop="sub_pm_type" label="授权书格式" />
<el-table-column
prop="verify_url"
label="授权书链接"
fixed="right"
width="180"
show-overflow-tooltip
/>
</el-table>
</template>
<script lang="ts" setup>
import axios from "axios";
import { onMounted, ref } from "vue";
const row_height = 50;
const limit_row = 5;
const tableData = ref([]);
onMounted(() => {
axios
.get("/api/order/list", {
params: {
limit: limit_row,
page: 2,
},
})
.then((res) => {
tableData.value = res.data;
});
});
const getRowHeight = (rowCount: number) => {
return rowCount * rowCount;
};
</script>
<style lang="scss" scoped>
.main-content {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
</style>

@ -1,13 +0,0 @@
<template>
<div>
user
</div>
</template>
<script setup lang='ts'>
</script>
<style lang='scss' scoped>
</style>

@ -1 +0,0 @@
/// <reference types="vite/client" />

@ -1,42 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

@ -1,31 +0,0 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { viteMockServe } from 'vite-plugin-mock'
// https://vitejs.dev/config/
export default defineConfig((config) => {
const { command } = config
return {
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
}),
vue(),
viteMockServe({
mockPath: 'mock',
enable: true
})
],
resolve: {
alias: {
'@': '/src'
}
}
}
})

@ -1,48 +0,0 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="mysql://root:yinhan123YH@101.42.154.98:3306/laoyou?schema=public"
# WX APP
WX_APP_ID="wx4c765f9dbc6e4ce8"
WX_APP_SECRET="dafe13d02bd6be50d8aa50d7f71ada72"
WX_LOGIN_URL="https://api.weixin.qq.com/sns/jscode2session"
# JWT
JWT_SECRET_KEY = "LAOYOU_JWT_SECRET"
JWT_EXPIRES_IN = "7d"
# COS
COS_IMG_BUCKET="laoyou-img-1300131488"
COS_FACE_BUCKET="laoyou-face-1300131488"
COS_REGION="ap-beijing"
COS_SECRET_ID="AKIDiN1RrOGD5qa7PYbQ6ZsESSDoHbigeOtE"
COS_SECRET_KEY="QjtzsX4SUK0V2Vq5Vkro8KMFyhT7dyBF"
# REDIS
REDIS_HOST= "localhost"
REDIS_PORT= 6379
# 高德
GAODE_KEY = "0f2c68a72e09ff9faf121364dab92c05"
WEATHER_URL = "https://restapi.amap.com/v3/weather/weatherInfo"
#REDIS_CONFIG
REDIS_KEY_EXPIRE= 604800 # 7days
#BAIDU
# NLP
BAIDU_NLP_API_KEY = "i4uY0g3UGAzQWK0HfTcEUCI8"
BAIDU_NLP_SECRET_KEY = "edRjTDyzgFxtzrg54ofaiTAckBn6XX2Z"
# FACE
BAIDU_FACE_API_KEY = "tLf2zfWOnxYbAYEF6rq561Yc"
BAIDU_FACE_SECRET_KEY = "0P4s7wc2o4h4kcADMcjMYezd670onr1n"
BAIDU_FACE_GROUP_ID = "common_user"

@ -1,25 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

@ -1,35 +0,0 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}

@ -1,73 +0,0 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ pnpm install
```
## Running the app
```bash
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
```
## Test
```bash
# unit tests
$ pnpm run test
# e2e tests
$ pnpm run test:e2e
# test coverage
$ pnpm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

@ -1,88 +0,0 @@
{
"name": "laoyou_backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"dev": "nest start --watch",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs-modules/ioredis": "^1.0.1",
"@nestjs/cache-manager": "^2.0.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^9.0.0",
"@nestjs/jwt": "^10.1.1",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/schedule": "^3.0.4",
"@nestjs/serve-static": "^4.0.0",
"@prisma/client": "^5.3.1",
"@types/passport-jwt": "^3.0.10",
"axios": "^1.5.1",
"cache-manager": "^4.1.0",
"cos-nodejs-sdk-v5": "^2.12.4",
"dotenv": "^16.3.1",
"ioredis": "^5.3.2",
"multer": "1.4.5-lts.1",
"passport-jwt": "^4.0.1",
"prisma": "^5.3.1",
"redis": "^4.6.10",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "29.5.0",
"@types/node": "18.15.11",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "29.5.0",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "29.0.5",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.2.0",
"typescript": "^4.7.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

File diff suppressed because it is too large Load Diff

@ -1,108 +0,0 @@
-- CreateTable
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`openid` VARCHAR(191) NOT NULL,
`nickname` VARCHAR(191) NOT NULL DEFAULT '微信用户',
`avatar` VARCHAR(191) NOT NULL DEFAULT 'https://laoyou-static-1300131488.cos.ap-beijing.myqcloud.com/user.png',
`sex` INTEGER NOT NULL,
`city` VARCHAR(191) NOT NULL,
`birthday` VARCHAR(191) NOT NULL,
`face_auth_status` BOOLEAN NOT NULL DEFAULT false,
`face_url` VARCHAR(191) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`commonShareCount` INTEGER NOT NULL DEFAULT 0,
`videoShareCount` INTEGER NOT NULL DEFAULT 0,
`commentCount` INTEGER NOT NULL DEFAULT 0,
UNIQUE INDEX `User_openid_key`(`openid`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CommonShare` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`title` VARCHAR(191) NOT NULL,
`content` TEXT NOT NULL,
`images` TEXT NOT NULL,
`cover` VARCHAR(191) NOT NULL,
`like` INTEGER NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`openid` VARCHAR(191) NOT NULL,
`key_words` VARCHAR(191) NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `VideoShare` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`title` VARCHAR(191) NOT NULL,
`video` VARCHAR(191) NOT NULL,
`like` INTEGER NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`openid` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CommonShareComment` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`content` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`openid` VARCHAR(191) NOT NULL,
`like` INTEGER NOT NULL DEFAULT 0,
`share_id` INTEGER NULL,
`fa_comment_id` INTEGER NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CommonShareLike` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`openid` VARCHAR(191) NOT NULL,
`share_id_list` VARCHAR(191) NOT NULL,
UNIQUE INDEX `CommonShareLike_openid_key`(`openid`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CommonShareCommentLike` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`openid` VARCHAR(191) NOT NULL,
`comment_id_list` VARCHAR(191) NOT NULL,
UNIQUE INDEX `CommonShareCommentLike_openid_key`(`openid`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `CommonShare` ADD CONSTRAINT `CommonShare_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `VideoShare` ADD CONSTRAINT `VideoShare_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CommonShareComment` ADD CONSTRAINT `CommonShareComment_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CommonShareComment` ADD CONSTRAINT `CommonShareComment_share_id_fkey` FOREIGN KEY (`share_id`) REFERENCES `CommonShare`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CommonShareComment` ADD CONSTRAINT `CommonShareComment_fa_comment_id_fkey` FOREIGN KEY (`fa_comment_id`) REFERENCES `CommonShareComment`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CommonShareLike` ADD CONSTRAINT `CommonShareLike_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CommonShareCommentLike` ADD CONSTRAINT `CommonShareCommentLike_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"

@ -1,99 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
openid String @unique
nickname String @default("微信用户")
avatar String @default("https://laoyou-static-1300131488.cos.ap-beijing.myqcloud.com/user.png")
sex Int
city String
birthday String
face_auth_status Boolean @default(false)
face_url String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// commonShare count
commonShareCount Int @default(0)
// videoShare count
videoShareCount Int @default(0)
// comment count
commentCount Int @default(0)
// 建立 User 与 Share 的关联关系
common_shares CommonShare[] @relation("commonShare")
video_shares VideoShare[] @relation("videoShare")
// VideoShare VideoShare[] @relation("videoShare")
Comment CommonShareComment[] @relation("user_comment")
CommonShareLike CommonShareLike[]
CommonShareCommentLike CommonShareCommentLike[]
}
model CommonShare {
id Int @id @default(autoincrement())
title String
content String @db.Text
images String @db.Text
cover String
like Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 使用 openid 字段与 User 表建立关联
openid String
user User @relation("commonShare", fields: [openid], references: [openid])
Comment CommonShareComment[] @relation("commshare_comment")
key_words String?
}
model VideoShare {
id Int @id @default(autoincrement())
title String
video String
like Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 使用 openid 字段与 User 表建立关联
openid String
user User @relation("videoShare", fields: [openid], references: [openid])
}
model CommonShareComment {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
openid String
like Int @default(0)
user User @relation("user_comment", fields: [openid], references: [openid])
share_id Int?
commonshare CommonShare? @relation("commshare_comment", fields: [share_id], references: [id])
sonComment CommonShareComment[] @relation("sonComment")
fa_comment_id Int?
fa_comment CommonShareComment? @relation("sonComment", fields: [fa_comment_id], references: [id])
}
model CommonShareLike {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
openid String @unique
share_id_list String
user User @relation(fields: [openid], references: [openid])
}
model CommonShareCommentLike {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
openid String @unique
comment_id_list String
user User @relation(fields: [openid], references: [openid])
}

@ -1,16 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { HttpService } from './modules/http/http.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService,
private readonly httpService: HttpService
) { }
@Get('hello')
getHello(): string {
return this.appService.getHello();
}
}

@ -1,44 +0,0 @@
import { CacheModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './modules/auth/auth.module';
import { CommentModule } from './modules/comment/comment.module';
import { FileUploadModule } from './modules/file-upload/file-upload.module';
import { HttpModule } from './modules/http/http.module';
import { PrismaModule } from './modules/prisma/prisma.module';
import { UserModule } from './modules/user/user.module';
import { CommonShareModule } from './modules/commonShare/common-share.module'
import { LikeModule } from './modules/like/like.module';
import { ScheduleModule } from './modules/schedule/schedule.module';
import { ApiModule } from './modules/api/api.module';
import { TestModule } from './test/test.module';
import { ConfigModule } from '@nestjs/config';
import { FaceModule } from './modules/face/face.module';
import { NewsModule } from './modules/news/news.module';
@Module({
imports: [
UserModule,
PrismaModule,
FileUploadModule,
AuthModule,
HttpModule,
CommentModule,
CommonShareModule,
LikeModule,
ScheduleModule,
ApiModule,
TestModule,
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env'
}),
FaceModule,
CacheModule.register(),
NewsModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

@ -1,6 +0,0 @@
import { UseGuards, applyDecorators } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
export function Auth() {
return applyDecorators(UseGuards(AuthGuard('jwt')))
}

@ -1,24 +0,0 @@
import { UseInterceptors, applyDecorators } from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { FileTooLargeException, InvalidFileTypeException } from "../error/fileError";
export function ImgUploadInterceptors() {
return applyDecorators(UseInterceptors(
FileInterceptor('img', {
fileFilter: (req, file, callback) => {
// 限制文件大小最大为 2mb
if (file.size > 2 * 1024 * 1024) {
callback(new FileTooLargeException(), false); // 拒绝上传
}
//
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp']; // 允许的图片上传类型
if (allowedMimes.includes(file.mimetype)) {
callback(null, true); // 允许上传
} else {
callback(new InvalidFileTypeException('图片'), false); // 拒绝上传
}
},
})
))
}

@ -1,2 +0,0 @@
export * from './auth.decorator'
export * from './imgUpload.decorator'

@ -1,8 +0,0 @@
class CommonShareDto {
readonly id?: number
readonly title: string
readonly content: string
readonly images: string
// 使用 openid 字段与 User 表建立关联
readonly openid: string
}

@ -1,7 +0,0 @@
class VideoShareDto {
readonly id: number
readonly title: String
readonly video: String
// 使用 openid 字段与 User 表建立关联
readonly openid: String
}

@ -1,10 +0,0 @@
export class UserDto {
id?: number;
openid?: string
nickname?: string;
avatar?: string;
birthday?: string;
local?: string;
sex?: number;
city?: string;
}

@ -1,4 +0,0 @@
export const enum API_TYPE {
NLP = "nlp",
FACE = "face",
}

@ -1,5 +0,0 @@
export const enum HttpCode {
SUCCESS = 201,
LOGIN_VERIFY_ERROR = 400,
LOGIN_ERROR = 401
}

@ -1,32 +0,0 @@
import { HttpException, HttpStatus } from '@nestjs/common';
// 图片格式错误
export class InvalidFileTypeException extends HttpException {
constructor(filetype: String) {
super(`上传${filetype}格式错误`, HttpStatus.BAD_REQUEST);
}
}
// 文件大小超过限制
export class FileTooLargeException extends HttpException {
constructor() {
super('文件大小超过限制', HttpStatus.BAD_REQUEST);
}
}
//空文件异常
export class EmptyFileException extends HttpException {
constructor() {
super('上传文件为空', HttpStatus.BAD_REQUEST);
}
}
// 文件上传异常
export class FileUploadException extends HttpException {
constructor() {
super('文件上传失败', HttpStatus.BAD_REQUEST);
}
}

@ -1,16 +0,0 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { HttpCode } from '../enum/httpCode'
// 图片格式错误
export class LoginVerifyException extends HttpException {
constructor() {
super('验证码错误', HttpCode.LOGIN_VERIFY_ERROR);
}
}
// 登录错误
export class LoginErrorException extends HttpException {
constructor() {
super('登录错误', HttpCode.LOGIN_ERROR);
}
}

@ -1,14 +0,0 @@
import { HttpException, HttpStatus } from "@nestjs/common";
export class ShareCreateException extends HttpException {
constructor() {
super(`分享上传失败`, HttpStatus.BAD_REQUEST);
}
}
// 分享查询失败
export class ShareQueryException extends HttpException {
constructor() {
super(`分享查询失败`, HttpStatus.BAD_REQUEST);
}
}

@ -1,7 +0,0 @@
import { HttpException, HttpStatus } from "@nestjs/common";
export class UserUpdateException extends HttpException {
constructor(message: string) {
super(message, HttpStatus.BAD_REQUEST);
}
}

@ -1,20 +0,0 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { LoginVerifyException } from 'src/error/login';
import { getSessionKey, hasUser } from 'src/store';
@Injectable()
export class AuthInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const openId = req.headers['openid'];
const seesionKey = req.headers['sessionkey'];
if (hasUser(openId) && getSessionKey(openId) === seesionKey) {
return next.handle();
} else {
throw new LoginVerifyException()
}
}
}

@ -1,12 +0,0 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { global_prefix, port } from './utils/config/index'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix(global_prefix);
await app.listen(port);
}
bootstrap();

@ -1,35 +0,0 @@
import { Body, Controller, Post, Req } from '@nestjs/common';
import { Auth } from 'src/decorator';
import { Request } from 'express';
import { ApiService } from './api.service';
@Controller('api')
export class ApiController {
constructor(
private readonly apiService: ApiService
) { }
@Post('wxrun')
@Auth()
wxrun(@Req() req: Request, @Body() body: any) {
const user = req.user as any;
const session_key = user.session_key
return this.apiService.wxrun(session_key, body.encryptedData, body.iv)
}
@Post('weather')
@Auth()
async getWeather() {
return this.apiService.getWeather();
}
@Post('getKeywords')
@Auth()
async getKeyWords(@Body('content') content: string, @Body('title') title: string) {
return (await this.apiService.getKeywords(title, content)).data;
}
}

@ -1,12 +0,0 @@
import { Global, Module } from '@nestjs/common';
import { ApiController } from './api.controller';
import { ApiService } from './api.service';
@Global()
@Module({
controllers: [ApiController],
providers: [ApiService],
exports: [ApiService]
})
export class ApiModule { }

@ -1,95 +0,0 @@
import { InjectRedis, Redis } from '@nestjs-modules/ioredis';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { API_TYPE } from 'src/enum/baidu';
import { BAIDU_ACCESS_TOKEN_KEY } from 'src/store/redis/keys';
import { WXBizDataCrypt } from '../../utils/fn/crypt';
import { HttpService } from '../http/http.service';
@Injectable()
export class ApiService {
private readonly weatherUrl: string;
private readonly appid: string;
private readonly gaodeKey: string;
// nlp
private readonly baidu_nlp_api_key: string;
private readonly baidu_nlp_secret_key: string;
// face
private readonly baidu_face_api_key: string;
private readonly baidu_face_secret_key: string;
constructor(
private configService: ConfigService,
@InjectRedis() private readonly redis: Redis,
private readonly httpService: HttpService,
) {
this.appid = configService.get<string>('WX_APP_ID');
this.gaodeKey = configService.get<string>('GAODE_KEY');
this.weatherUrl = configService.get<string>('WEATHER_URL');
this.baidu_nlp_api_key = configService.get<string>('BAIDU_NLP_API_KEY');
this.baidu_nlp_secret_key = configService.get<string>('BAIDU_NLP_SECRET_KEY');
this.baidu_face_api_key = configService.get<string>('BAIDU_FACE_API_KEY');
this.baidu_face_secret_key = configService.get<string>('BAIDU_FACE_SECRET_KEY');
}
wxrun(session_key: string, encryptedData: string, iv: string) {
const pc = new WXBizDataCrypt(process.env.WX_APP_ID, session_key)
const data = pc.decryptData(encryptedData, iv)
return data;
}
// 天津市 120000 022
async getWeather() {
return (await this.httpService.get(this.weatherUrl, {
params: {
key: this.gaodeKey,
city: '120000',
}
}) as any).data;
}
// 获得百度 accessToken
async getBaiduOauthAccessToken(type: string, ak: string, sk: string) {
const access_token_from_redis = await this.redis.get(BAIDU_ACCESS_TOKEN_KEY + ':' + type);
if (access_token_from_redis) {
return access_token_from_redis;
}
const res = await this.httpService.get('https://aip.baidubce.com/oauth/2.0/token', {
params: {
grant_type: 'client_credentials',
client_id: ak,
client_secret: sk
}
})
const expires_in = (res.data as any).expires_in;
const access_token = (res.data as any).access_token;
// 存入redis
this.redis.set(BAIDU_ACCESS_TOKEN_KEY + ':' + type, access_token, 'EX', expires_in);
return access_token;
}
// 根据文章标题和内容提取关键词
async getKeywords(title: string, content: string) {
return await this.httpService.post<TopicData>('https://aip.baidubce.com/rpc/2.0/nlp/v1/topic',
{
title,
content
},
{
params: {
access_token: await this.getBaiduOauthAccessToken(API_TYPE.NLP, this.baidu_nlp_api_key, this.baidu_nlp_secret_key),
charset: 'UTF-8'
},
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
})
}
}

@ -1,51 +0,0 @@
import { Body, Controller, Get, Post, Res } from '@nestjs/common';
import { Response } from 'express';
import { Auth } from 'src/decorator/auth.decorator';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService
) { }
//登陆
@Post('login')
async login(@Body('code') code: string, @Res() res: Response) {
// 在这里进行登录逻辑,验证用户信息等
try {
const login_info = (await this.authService.login(code));
// 设置 Access-Control-Expose-Headers : 'Key'
// 用于前端获取自定义的响应头
res.header('Access-Control-Expose-Headers', 'authorization,token-max-age,username');
res.header('authorization', `Bearer ${login_info.token}`);
res.header('token-max-age', process.env.JWT_EXPIRES_IN);
return res.status(201).json({
message: 'Login successful',
user: login_info.user
});
} catch (error) {
return res.status(401).json({ message: 'Unauthorized' });
}
}
@Post('faceLogin')
async faceLogin(@Body('face_url') face_url: string, @Res() res: Response) {
try {
const login_info = (await this.authService.faceLogin(face_url));
// 设置 Access-Control-Expose-Headers : 'Key'
// 用于前端获取自定义的响应头
res.header('Access-Control-Expose-Headers', 'authorization,token-max-age,username');
res.header('authorization', `Bearer ${login_info.token}`);
res.header('token-max-age', process.env.JWT_EXPIRES_IN);
return res.status(201).json({
message: 'Login successful',
user: login_info.user
});
} catch (error) {
return res.status(401).json({ message: 'Unauthorized' });
}
}
}

@ -1,28 +0,0 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config'
import { UserService } from '../user/user.service';
import { JwtStrategy } from 'src/strategy/jwt.strategy';
import COS from 'cos-nodejs-sdk-v5'
import { FaceService } from '../face/face.service';
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async () => {
return {
secret: process.env.JWT_SECRET_KEY,
signOptions: {
expiresIn: process.env.JWT_EXPIRES_IN
}
}
}
})
],
controllers: [AuthController],
providers: [AuthService, UserService, JwtStrategy, FaceService]
})
export class AuthModule { }

@ -1,90 +0,0 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { LoginErrorException } from 'src/error/login';
import { HttpService } from '../http/http.service';
import { UserService } from '../user/user.service';
import { ApiService } from '../api/api.service';
import { filterObject } from '../../utils/fn/format'
import { FaceService } from '../face/face.service';
@Injectable()
export class AuthService {
private readonly wx_login_url = process.env.WX_LOGIN_URL;
constructor(
private readonly jwtService: JwtService,
private readonly httpService: HttpService,
private readonly userService: UserService,
private readonly faceService: FaceService
) { }
async login(code: string) {
try {
const res = await this.httpService.get<WXLoginResponse>(this.wx_login_url, {
params: {
appid: process.env.WX_APP_ID,
secret: process.env.WX_APP_SECRET,
js_code: code,
grant_type: 'authorization_code'
}
})
const login_info = res.data;
const openid = login_info.openid;
let user = await this.userService.findUserByOpenId(openid);
if (user === null) {
// 创建用户
user = await this.userService.createUser(openid);
}
return {
user,
token: (await this.token(login_info)).token
}
} catch (error) {
console.log(error);
throw new LoginErrorException();
}
}
// {
// session_key: 'iEaVjg1JmLWrUI2dHfuYpg==',
// openid: 'oyBXy5ZPz7etmnwy34TICz2QT0O4'
// }
async faceLogin(face_url: string) {
try {
const auth_res = await this.faceService.faceMatch(face_url);
// 完全不匹配
if (auth_res.result.user_list.length === 0) {
throw new LoginErrorException();
}
// 匹配度低于80
if (auth_res.result.user_list[0].score < 80) {
throw new LoginErrorException();
}
const openid = auth_res.result.user_list[0].user_id;
// 查询用户
let user = await this.userService.findUserByOpenId(openid);
// 用户不存在
if (user === null) {
throw new LoginErrorException();
}
return {
user,
token: (await this.token({ openid })).token
}
} catch (error) {
console.log('face login:', error);
throw new LoginErrorException();
}
}
async token(payload: any) {
return {
token: await this.jwtService.signAsync(payload)
}
}
}

@ -1,44 +0,0 @@
import { Body, Controller, Get, ParseIntPipe, Post, Query, Req } from '@nestjs/common';
import { Auth } from '../../decorator';
import { Request } from 'express';
import { CommentService } from './comment.service';
@Controller('comment')
export class CommentController {
constructor(
private readonly commentService: CommentService,
) { }
@Post('add')
@Auth()
createPost(@Req() req: Request, @Body() comment) {
const user = req.user as any;
const openid = user.openid;
comment.openid = openid;
return this.commentService.createComment(comment);
}
@Get('getCommonShareComment')
@Auth()
getCommonShareComment(@Query('share_id', ParseIntPipe) common_share_id: number) {
return this.commentService.getCommentByShareId(common_share_id);
}
@Get('getUserSelfCommentCnt')
@Auth()
getSelfCommentCount(@Req() req: Request) {
const user = req.user as any;
const openid = user.openid;
return this.commentService.getSelfCommentCount(openid);
}
@Get('list')
@Auth()
getCommentList(@Query('page', ParseIntPipe) page: number, @Query('size', ParseIntPipe) size: number, @Query('openid') openid?: string) {
return this.commentService.getCommentList(page, size, openid);
}
}

@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { CommentController } from './comment.controller';
import { CommentService } from './comment.service';
import { UserService } from 'src/modules/user/user.service';
@Module({
controllers: [CommentController],
providers: [CommentService,UserService]
})
export class CommentModule {}

@ -1,171 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { UserService } from '../user/user.service';
import { Prisma } from '@prisma/client';
@Injectable()
export class CommentService {
constructor(
private readonly prisma: PrismaService,
private readonly userService: UserService
) { }
// {
// content: '跟评1',
// share_type: 0,
// common_share_id: 11,
// fa_comment_id: 5,
// openid: 'oyBXy5ZPz7etmnwy34TICz2QT0O4'
// }
async createComment(data) {
try {
// have fa_comment_id
let create_comment_transaction;
if (data.fa_comment_id) {
create_comment_transaction = this.prisma.commonShareComment.create({
data: {
content: data.content,
user: {
connect: {
openid: data.openid
}
},
commonshare: {
connect: {
id: data.common_share_id
}
},
fa_comment: {
connect: {
id: data.fa_comment_id
}
}
},
include: {
user: true
}
})
} else {
create_comment_transaction = this.prisma.commonShareComment.create(
{
data: {
content: data.content,
user: {
connect: {
openid: data.openid
}
},
commonshare: {
connect: {
id: data.common_share_id
}
},
},
include: {
user: true
}
}
)
}
// 建立事务
const transaction = await this.prisma.$transaction([
// 创建评论
create_comment_transaction,
// 用户评论数+1
this.prisma.user.update({
where: {
openid: data.openid,
},
data: {
commentCount: {
increment: 1
}
}
})
]);
return transaction[0];
} catch (error) {
console.log(error);
throw new Error('评论失败')
}
}
// 根据share id获取评论
async getCommentByShareId(id) {
try {
return await this.prisma.commonShareComment.findMany({
where: {
commonshare: {
id: id
}
},
include: {
user: true
}
});
} catch (error) {
console.log(error);
throw new Error('获取评论失败')
}
}
// 根据openid 获取用户评论总数
async getSelfCommentCount(openid: string) {
try {
return await this.prisma.commonShareComment.count({
where: {
user: {
openid
}
}
})
} catch (error) {
throw new Error('获取评论总数失败')
}
}
// 分页获取评论列表
async getCommentList(page: number, size: number, openid?: string) {
try {
const where_condition: Prisma.CommonShareCommentWhereInput = {};
// 如果有openid 则查询该用户的评论
if (openid) {
where_condition.user = {
openid: openid
}
}
const comment_list = this.prisma.commonShareComment.findMany({
where: where_condition,
skip: (page - 1) * size,
take: size,
orderBy: {
createdAt: 'desc'
},
include: {
user: {
select: {
id: true,
nickname: true,
avatar: true,
openid: true,
},
},
commonshare: true,
}
});
const comment_total_count = this.prisma.commonShareComment.count({
where: where_condition
});
const res = await Promise.all([comment_list, comment_total_count]);
return {
comment_list: res[0],
total_count: res[1]
}
} catch (error) {
throw new Error('获取评论列表失败')
}
}
}

@ -1,38 +0,0 @@
import { Body, Controller, Get, ParseIntPipe, Post, Query, Req } from '@nestjs/common';
import { Request } from 'express';
import { Auth } from 'src/decorator/auth.decorator';
import { CommonShareService } from './common-share.service';
@Controller('commonshare')
export class CommonShareController {
constructor(
private readonly commonShareService: CommonShareService,
) { }
@Post('add/commonshare')
@Auth()
async commitShare(@Req() req: Request, @Body() body) {
return this.commonShareService.createCommonShare(req, body);
}
@Get('get/commonshare')
@Auth()
async getCommonShare(@Query('page', ParseIntPipe) page, @Query('size', ParseIntPipe) size, @Query('openid') openid) {
openid = openid === 'undefined' ? undefined : openid;
return await this.commonShareService.findCommonShare(page, size, openid)
}
@Get('self/commonshare/cnt')
@Auth()
async getSelfCommonShareCount(@Req() req: Request) {
const user = req.user as any;
const openid = user.openid;
return await this.commonShareService.getSelfCommonShareCount(openid);
}
}

@ -1,17 +0,0 @@
import { Module } from '@nestjs/common';
import { CommonShareController } from './common-share.controller';
import { CommonShareService } from './common-share.service';
import { UserService } from 'src/modules/user/user.service';
import { LikeModule } from '../like/like.module';
import { LikeService } from '../like/like.service';
import { ApiService } from '../api/api.service';
import { ApiModule } from '../api/api.module';
@Module({
imports: [
LikeModule
],
controllers: [CommonShareController],
providers: [CommonShareService, UserService, LikeService]
})
export class CommonShareModule { }

@ -1,176 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { Request } from 'express';
import { ShareCreateException, ShareQueryException } from 'src/error/post';
import { PrismaService } from 'src/modules/prisma/prisma.service';
import { LikeService } from '../like/like.service';
import { ApiService } from '../api/api.service';
@Injectable()
export class CommonShareService {
private readonly like_commonshare_shareid_key = process.env.LIKE_COMMONSHARE_SHAREID_KEY;
constructor(
private readonly prisma: PrismaService,
private readonly likeService: LikeService,
private apiService: ApiService
) { }
// {
// pic_list: [
// 'https://laoyou-img-1300131488.cos.ap-beijing.myqcloud.com/183493377dd45d1a34c84228c48fa8a9.jpg'
// ],
// common_share_title: '这是一个文章标题',
// common_share_content: '这是正文部分',
// nickname: '微信用户',
// avatar: 'https://laoyou-static-1300131488.cos.ap-beijing.myqcloud.com/user.png'
// }
// 创建commonShare
async createCommonShare(req: Request, body: any) {
const user = req.user as any;
const openid = user.openid;
const imagesPath = body.pic_list.join(',');
const content = body.common_share_content;
const title = body.common_share_title;
let key_words: TopicData;
const item_list = [];
try {
key_words = (await this.apiService.getKeywords(title, content)).data;
const article_topic = this.getArticleTopic(key_words);
const commonShare: Prisma.CommonShareCreateInput = {
title: body.common_share_title,
content: body.common_share_content,
images: imagesPath,
cover: body.pic_list[0],
like: 0,
key_words: article_topic,
user: {
connect: {
openid
}
}
}
try {
const transaction = await this.prisma.$transaction([
this.prisma.commonShare.create({
data: commonShare
}),
this.prisma.user.update({
where: {
openid
},
data: {
commonShareCount: {
increment: 1
}
}
})
]);
return transaction[0];
} catch (error) {
console.log(error);
throw new ShareCreateException();
}
} catch (error) {
console.log(error);
}
}
getArticleTopic(topicData: TopicData) {
const lv1_tag_list = topicData.item.lv1_tag_list;
const lv2_tag_list = topicData.item.lv2_tag_list;
const tag_list = lv1_tag_list.concat(lv2_tag_list);
const tag_list_str = tag_list.map(item => {
return item.tag;
}).join(',');
return tag_list_str;
}
// 分页查询commonShare
async findCommonShare(page: number, pageSize: number, openid?: string | undefined) {
// 查询post 并附带user信息
try {
const where_condition = openid ? {
user: {
openid: openid
}
} : undefined;
const commonshare_list = await this.prisma.commonShare.findMany({
where: where_condition,
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: {
createdAt: 'desc'
},
include: {
user: {
select: {
city: true,
nickname: true,
avatar: true,
openid: true
}
}
}
})
const promises_commonshare_list = Promise.all(commonshare_list.map(async item => {
// 从缓存中获取like数量
// 如果缓存中没有,就从数据库中获取
const share_like_cnt_in_redis = await this.likeService.get_commonshare_like_cnt(item.id)
if (share_like_cnt_in_redis === null) {
// 将db中的like数量存入redis
this.likeService.change_commonshare_like_cnt(item.id, item.like);
} else {
item.like = parseInt(share_like_cnt_in_redis);
}
return item;
}))
const total_count = this.prisma.commonShare.count({
where: where_condition
});
const promises = await Promise.all([promises_commonshare_list, total_count]);
return {
commonshare_list: promises[0],
total_count: promises[1]
}
} catch (error) {
throw new ShareQueryException();
}
}
// 根据openid查询commonShare数量
async getSelfCommonShareCount(openid: string) {
try {
return await this.prisma.commonShare.count({
where: {
user: {
openid
}
}
})
} catch (error) {
throw new ShareQueryException();
}
}
}

@ -1,24 +0,0 @@
import { Body, Controller, Post, Req } from '@nestjs/common';
import { Request } from 'express';
import { FaceService } from './face.service';
import { Auth } from 'src/decorator';
@Controller('face')
export class FaceController {
constructor(
private readonly faceService: FaceService
) { }
@Post('register')
@Auth()
async register(@Body('face_url') face_url: string, @Req() req: Request) {
return this.faceService.faceRegister(face_url, req.user);
}
@Post('match')
async match(@Body('face_url') face_url: string) {
return this.faceService.faceMatch(face_url);
}
}

@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { FaceController } from './face.controller';
import { FaceService } from './face.service';
@Module({
controllers: [FaceController],
providers: [FaceService]
})
export class FaceModule { }

@ -1,72 +0,0 @@
import { InjectRedis, Redis } from '@nestjs-modules/ioredis';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { API_TYPE } from 'src/enum/baidu';
import { BAIDU_ACCESS_TOKEN_KEY } from 'src/store/redis/keys';
import { WXBizDataCrypt } from '../../utils/fn/crypt';
import { HttpService } from '../http/http.service';
import { ApiService } from '../api/api.service';
import { UserService } from '../user/user.service';
@Injectable()
export class FaceService {
private readonly baidu_face_api_key: string;
private readonly baidu_face_secret_key: string;
private readonly baidu_face_group_id: string;
constructor(
private configService: ConfigService,
private readonly httpService: HttpService,
private readonly userService: UserService,
private readonly apiService: ApiService,
@InjectRedis() private readonly redis: Redis,
) {
this.baidu_face_api_key = configService.get<string>('BAIDU_FACE_API_KEY');
this.baidu_face_secret_key = configService.get<string>('BAIDU_FACE_SECRET_KEY');
this.baidu_face_group_id = configService.get<string>('BAIDU_FACE_GROUP_ID');
}
// 创建用户组
// 人脸注册
async faceRegister(face_url: string, user: any) {
try {
const access_token = await this.apiService.getBaiduOauthAccessToken(API_TYPE.FACE, this.baidu_face_api_key, this.baidu_face_secret_key);
const openid = user.openid;
const res = await this.httpService.post('https://aip.baidubce.com/rest/2.0/face/v3/faceset/user/add?access_token=' + access_token, {
group_id: this.baidu_face_group_id,
image: face_url,
image_type: "URL",
user_id: openid,
});
this.userService.updateUser({
openid: openid,
face_auth_status: true
});
return res.data;
} catch (error) {
console.log(error);
throw new HttpException('人脸注册失败', HttpStatus.BAD_REQUEST);
}
}
// 人脸匹配
async faceMatch(face_url: string): Promise<FaceAuthData> {
try {
const access_token = await this.apiService.getBaiduOauthAccessToken(API_TYPE.FACE, this.baidu_face_api_key, this.baidu_face_secret_key);
const res = await this.httpService.post('https://aip.baidubce.com/rest/2.0/face/v3/search?access_token=' + access_token, {
image: face_url,
image_type: "URL",
group_id_list: this.baidu_face_group_id,
})
return res.data as any;
} catch (error) {
throw new HttpException('人脸匹配失败', HttpStatus.BAD_REQUEST)
}
}
}

@ -1,26 +0,0 @@
import { Controller, Post, UploadedFile } from '@nestjs/common';
import { ImgUploadInterceptors } from '../../decorator';
import { FileUploadService } from './file-upload.service';
@Controller('upload')
export class FileUploadController {
constructor(
private readonly fileUploadService: FileUploadService
) { }
@Post('img')
@ImgUploadInterceptors()
async uploadImg(@UploadedFile() file) {
return this.fileUploadService.uploadImg(file, "img");
}
@Post('face')
@ImgUploadInterceptors()
async uploadFace(@UploadedFile() file) {
// TODO 进行节流判断
return this.fileUploadService.uploadImg(file, "face");
}
}

@ -1,22 +0,0 @@
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express/multer';
import { FileUploadController } from './file-upload.controller';
import { FileUploadService } from './file-upload.service';
import * as COS from 'cos-nodejs-sdk-v5'
@Module({
imports: [MulterModule],
controllers: [FileUploadController],
providers: [FileUploadService,
{
provide: 'COS',
useFactory: () => {
return new COS({
SecretId: process.env.COS_SECRET_ID,
SecretKey: process.env.COS_SECRET_KEY
})
}
},
],
})
export class FileUploadModule { }

@ -1,71 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import * as COS from 'cos-nodejs-sdk-v5';
import { extname } from 'path';
import { EmptyFileException, FileUploadException } from '../../error/fileError';
import { ConfigService } from '@nestjs/config';
type FILE_TYPE = 'img' | 'face';
@Injectable()
export class FileUploadService {
private readonly bucketImg: string;
private readonly bucket_face: string;
private readonly region: string;
constructor(
private readonly config: ConfigService,
@Inject('COS') private readonly cos: COS,
) {
this.bucketImg = config.get<string>('COS_IMG_BUCKET');
this.bucket_face = config.get<string>('COS_FACE_BUCKET');
this.region = config.get<string>('COS_REGION');
}
async uploadImg(file: any, file_type: FILE_TYPE): Promise<string> {
// 处理上传的文件
if (!file) {
throw new EmptyFileException();
}
let buket_name;
switch (file_type) {
case 'img':
buket_name = this.bucketImg;
break;
case 'face':
buket_name = this.bucket_face;
break;
default:
throw new FileUploadException();
}
const randomName = this.generateRandomName();
const params = {
Bucket: buket_name,
Region: this.region,
Key: randomName + extname(file.originalname),
Body: file.buffer
} as COS.PutObjectParams;
try {
await this.cos.putObject(params, (err, data) => {
if (err) {
console.log(err)
throw new FileUploadException();
}
})
return `https://${buket_name}.cos.${this.region}.myqcloud.com/${randomName}${extname(file.originalname)}`;
} catch (err) {
throw new FileUploadException();
}
}
generateRandomName(): string {
return Array(32)
.fill(null)
.map(() => Math.floor(Math.random() * 16).toString(16))
.join('');
}
}

@ -1,9 +0,0 @@
import { Global, Module } from '@nestjs/common';
import { HttpService } from './http.service';
@Global()
@Module({
providers: [HttpService],
exports: [HttpService]
})
export class HttpModule { }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save