This commit is contained in:
丹尼尔
2026-03-10 16:44:07 +08:00
commit 36101a4405
10 changed files with 228 additions and 0 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
PORT=3000
WECHAT_UPSTREAM_BASE_URL=http://your-wechat-server-host:port

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY tsconfig.json ./tsconfig.json
COPY src ./src
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY .env.example ./ ./
EXPOSE 3000
CMD [\"node\", \"dist/server.js\"]

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "wechat-admin-backend",
"version": "1.0.0",
"description": "WeChat backend admin service based on 8069 swagger with login support",
"main": "dist/server.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js"
},
"dependencies": {
"axios": "^1.7.7",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.2",
"morgan": "^1.10.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"ts-node-dev": "^2.0.0",
"typescript": "^5.7.0"
}
}

0
read.md Normal file
View File

31
run-docker.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -e
IMAGE_NAME="wechat-admin-backend"
CONTAINER_NAME="wechat-admin-backend"
PORT="${PORT:-3000}"
echo "Building Docker image: ${IMAGE_NAME}..."
docker build -t "${IMAGE_NAME}" .
echo "Stopping and removing existing container (if any)..."
if [ "$(docker ps -aq -f name=${CONTAINER_NAME})" ]; then
docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
fi
ENV_FILE=".env"
if [ ! -f "${ENV_FILE}" ]; then
echo "Env file ${ENV_FILE} not found, copying from .env.example ..."
cp .env.example "${ENV_FILE}"
fi
echo "Running container ${CONTAINER_NAME} on port ${PORT}..."
docker run -d \
--name "${CONTAINER_NAME}" \
--env-file "${ENV_FILE}" \
-p "${PORT}:3000" \
"${IMAGE_NAME}"
echo "Container started. Health check: curl http://localhost:${PORT}/health"

28
src/server.ts Normal file
View File

@@ -0,0 +1,28 @@
import express from 'express';
import cors from 'cors';
import morgan from 'morgan';
import dotenv from 'dotenv';
import { authRouter } from './wechatAuth';
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
app.get('/health', (_req, res) => {
res.json({ status: 'ok' });
});
app.use('/api/auth', authRouter);
const port = process.env.PORT || 3000;
app.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`WeChat admin backend listening on port ${port}`);
});

100
src/wechatAuth.ts Normal file
View File

@@ -0,0 +1,100 @@
import express from 'express';
import axios from 'axios';
const router = express.Router();
const BASE_URL = process.env.WECHAT_UPSTREAM_BASE_URL || 'http://localhost:8080';
router.post('/qrcode', async (req, res) => {
const { key, proxy, ipadOrMac, check } = req.body as {
key: string;
proxy?: string;
ipadOrMac?: string;
check?: boolean;
};
if (!key) {
return res.status(400).json({ error: 'key is required' });
}
try {
const response = await axios.post(
`${BASE_URL}/login/GetLoginQrCodeNewDirect`,
{
Proxy: proxy ?? '',
IpadOrmac: ipadOrMac ?? '',
Check: check ?? false,
},
{ params: { key } },
);
return res.json(response.data);
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error fetching login qrcode', err);
return res.status(502).json({ error: 'failed_to_get_qrcode' });
}
});
router.get('/status', async (req, res) => {
const { key } = req.query as { key?: string };
if (!key) {
return res.status(400).json({ error: 'key is required' });
}
try {
const response = await axios.get(`${BASE_URL}/login/GetLoginStatus`, {
params: { key },
});
return res.json(response.data);
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error getting login status', err);
return res.status(502).json({ error: 'failed_to_get_status' });
}
});
router.get('/scan-status', async (req, res) => {
const { key } = req.query as { key?: string };
if (!key) {
return res.status(400).json({ error: 'key is required' });
}
try {
const response = await axios.get(`${BASE_URL}/login/CheckLoginStatus`, {
params: { key },
});
return res.json(response.data);
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error checking scan status', err);
return res.status(502).json({ error: 'failed_to_check_scan_status' });
}
});
router.post('/logout', async (req, res) => {
const { key } = req.body as { key?: string };
if (!key) {
return res.status(400).json({ error: 'key is required' });
}
try {
const response = await axios.get(`${BASE_URL}/login/LogOut`, {
params: { key },
});
return res.json(response.data);
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error logging out', err);
return res.status(502).json({ error: 'failed_to_logout' });
}
});
export const authRouter = router;

1
swagger.json Normal file

File diff suppressed because one or more lines are too long

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src"]
}

BIN
登录流程.docx Normal file

Binary file not shown.