feat: 添加短链管理功能,包括创建、更新、删除和列表接口
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
"pub:kevisual": "npm run build && npm run deploy:kevisual && npm run reload:kevisual",
|
"pub:kevisual": "npm run build && npm run deploy:kevisual && npm run reload:kevisual",
|
||||||
"start": "pm2 start dist/app.js --name code-center",
|
"start": "pm2 start dist/app.js --name code-center",
|
||||||
"client:start": "pm2 start apps/code-center/dist/app.js --name code-center",
|
"client:start": "pm2 start apps/code-center/dist/app.js --name code-center",
|
||||||
|
"import-data": "bun run scripts/import-data.ts",
|
||||||
"studio": "npx drizzle-kit studio",
|
"studio": "npx drizzle-kit studio",
|
||||||
"drizzle:migrate": "npx drizzle-kit migrate",
|
"drizzle:migrate": "npx drizzle-kit migrate",
|
||||||
"drizzle:push": "npx drizzle-kit push",
|
"drizzle:push": "npx drizzle-kit push",
|
||||||
|
|||||||
78
scripts/import-data.ts
Normal file
78
scripts/import-data.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 导入 short-link JSON 数据到数据库
|
||||||
|
* 运行: bun run scripts/import-data.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
import { shortLink } from '@/db/schemas/n-code-schema.ts';
|
||||||
|
import { useConfig } from '@kevisual/use-config';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
const config = useConfig() as any;
|
||||||
|
const DATABASE_URL = config.DATABASE_URL || process.env.DATABASE_URL || '';
|
||||||
|
if (!DATABASE_URL) {
|
||||||
|
console.error('缺少 DATABASE_URL 配置');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = drizzle(DATABASE_URL);
|
||||||
|
|
||||||
|
// 读取 JSON 数据
|
||||||
|
const jsonPath = resolve(import.meta.dir, 'ncode-list.json');
|
||||||
|
const rawData = JSON.parse(readFileSync(jsonPath, 'utf-8')) as Array<{
|
||||||
|
code: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
description: string;
|
||||||
|
slug: string;
|
||||||
|
tags: string[];
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
userId: string;
|
||||||
|
version: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
async function importData() {
|
||||||
|
console.log(`准备导入 ${rawData.length} 条 short-link 数据...`);
|
||||||
|
|
||||||
|
let inserted = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
|
for (const item of rawData) {
|
||||||
|
const userId = item.userId && uuidRegex.test(item.userId) ? item.userId : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.insert(shortLink)
|
||||||
|
.values({
|
||||||
|
slug: item.slug,
|
||||||
|
code: item.code,
|
||||||
|
type: item.type || 'link',
|
||||||
|
version: item.version || '1.0',
|
||||||
|
title: item.title || '',
|
||||||
|
description: item.description || '',
|
||||||
|
tags: item.tags ?? [],
|
||||||
|
data: item.data ?? {},
|
||||||
|
userId: userId as any,
|
||||||
|
})
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
console.log(` ✓ 导入: slug=${item.slug}, code=${item.code}, title=${item.title}`);
|
||||||
|
inserted++;
|
||||||
|
} catch (err: any) {
|
||||||
|
const cause = err.cause || err;
|
||||||
|
console.warn(` ✗ 跳过: slug=${item.slug}, code=${item.code} — ${cause.message || err.message}`);
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n完成: 成功 ${inserted} 条,跳过 ${skipped} 条`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
importData().catch((err) => {
|
||||||
|
console.error('导入失败:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
104
scripts/ncode-list.json
Normal file
104
scripts/ncode-list.json
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "anwjgg",
|
||||||
|
"data": {
|
||||||
|
"link": "https://kevisual.cn/root/name-card/"
|
||||||
|
},
|
||||||
|
"description": "这是一个测试码",
|
||||||
|
"slug": "pmzsq4gp4g",
|
||||||
|
"tags": [],
|
||||||
|
"title": "测试码",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "0e700dc8-90dd-41b7-91dd-336ea51de3d2",
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "0gmn12",
|
||||||
|
"data": {
|
||||||
|
"link": "https://kevisual.cn/root/v1/ha-api?path=ha&key=open-balcony-light",
|
||||||
|
"permission": {
|
||||||
|
"share": "public"
|
||||||
|
},
|
||||||
|
"useOwnerToken": true
|
||||||
|
},
|
||||||
|
"description": "test 阳台灯",
|
||||||
|
"slug": "3z1nbdogew",
|
||||||
|
"tags": [],
|
||||||
|
"title": "阳台灯",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "0e700dc8-90dd-41b7-91dd-336ea51de3d2",
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "abc111",
|
||||||
|
"data": {
|
||||||
|
"link": "https://kevisual.cn/root/nfc/",
|
||||||
|
"permission": {
|
||||||
|
"share": "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "nfc link",
|
||||||
|
"slug": "0000000001",
|
||||||
|
"tags": [],
|
||||||
|
"title": "nfc link",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "",
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ej73jm",
|
||||||
|
"data": {
|
||||||
|
"link": "https://kevisual.cn/root/nfc/",
|
||||||
|
"permission": {
|
||||||
|
"share": "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "nfc link",
|
||||||
|
"slug": "001",
|
||||||
|
"tags": [],
|
||||||
|
"title": "nfc link",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "",
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "09dd42",
|
||||||
|
"data": {
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"description": "算法基于七卡瓦,自建",
|
||||||
|
"title": "beads(kevisual.cn)",
|
||||||
|
"url": "https://kevisual.cn/root/beads/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "算法很不错,图片转图纸很好",
|
||||||
|
"title": "拼豆七卡瓦(zippland.com)",
|
||||||
|
"url": "https://perlerbeads.zippland.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "功能偏向PS,画板类,像素类",
|
||||||
|
"title": "拼豆像素格子(zwpyyds.com)",
|
||||||
|
"url": "https://www.zwpyyds.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "编辑不错,拼豆社区",
|
||||||
|
"title": "我嘞个豆(ohmybead.cn)",
|
||||||
|
"url": "https://ohmybead.cn/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"link": "https://kevisual.cn/root/nfc/way/",
|
||||||
|
"permission": {
|
||||||
|
"share": "public"
|
||||||
|
},
|
||||||
|
"title": "拼豆图纸自取方案",
|
||||||
|
"type": "html-render"
|
||||||
|
},
|
||||||
|
"description": "Pindou",
|
||||||
|
"slug": "pindou",
|
||||||
|
"tags": [],
|
||||||
|
"title": "Pindou",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "",
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
import { pgTable, uuid, jsonb, timestamp, text } from "drizzle-orm/pg-core";
|
import { pgTable, uuid, jsonb, timestamp, text } from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel, InferInsertModel, desc } from "drizzle-orm";
|
import { InferSelectModel, InferInsertModel, desc } from "drizzle-orm";
|
||||||
export * from './drizzle/schema.ts';
|
export * from './drizzle/schema.ts';
|
||||||
|
|
||||||
|
export * from './schemas/n-code-schema.ts'
|
||||||
57
src/db/schemas/n-code-schema.ts
Normal file
57
src/db/schemas/n-code-schema.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { pgTable, serial, text, jsonb, varchar, timestamp, unique, uuid, doublePrecision, json, integer, boolean, index, uniqueIndex, pgEnum } from "drizzle-orm/pg-core"
|
||||||
|
import { desc, sql, sum } from "drizzle-orm"
|
||||||
|
|
||||||
|
export const shortLink = pgTable("n_code_short_link", {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
// 对外暴露的唯一业务 ID,nanoid 生成
|
||||||
|
slug: text("slug").notNull(),
|
||||||
|
// 协作码,管理员才能编辑, 6-12 位随机字符串,唯一
|
||||||
|
code: text("code").notNull().default(''),
|
||||||
|
// 码的类型,link, agent,默认值为 link
|
||||||
|
type: text("type").notNull().default("link"),
|
||||||
|
version: text("version").notNull().default('1.0.0'),
|
||||||
|
|
||||||
|
title: text("title").notNull().default(''),
|
||||||
|
description: text("description").notNull().default(''),
|
||||||
|
tags: jsonb().default([]),
|
||||||
|
data: jsonb().default({}),
|
||||||
|
|
||||||
|
userId: uuid(),
|
||||||
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
|
|
||||||
|
}, (table) => [
|
||||||
|
uniqueIndex("n_code_short_idx_slug").on(table.slug),
|
||||||
|
uniqueIndex("n_code_short_idx_code").on(table.code)
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const n5Make = pgTable("n_code_make", {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
slug: text("slug").notNull(),
|
||||||
|
resources: jsonb().default([]),
|
||||||
|
|
||||||
|
userId: uuid(),
|
||||||
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
|
}, (table) => [
|
||||||
|
uniqueIndex("n_code_make_idx_slug").on(table.slug),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const n5Shop = pgTable("n_code_shop", {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
slug: text("slug").notNull(),
|
||||||
|
title: text("title").notNull(), // 商品标题
|
||||||
|
tags: jsonb(), // 商品标签
|
||||||
|
link: text("link"), // 商品链接
|
||||||
|
description: text("description").notNull().default(''), // 商品描述
|
||||||
|
data: jsonb(), // 其他商品发货信息等
|
||||||
|
platform: text("platform").notNull(), // 商品平台,如 "淘宝", "抖音", "小红书" 等
|
||||||
|
userinfo: text("userinfo"), // 卖家信息,如 "店铺名称", "联系方式" 等
|
||||||
|
orderLink: text("orderLink").notNull(), // 订单链接
|
||||||
|
|
||||||
|
userId: uuid(),
|
||||||
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
|
}, (table) => [
|
||||||
|
uniqueIndex("n_code_shop_idx_slug").on(table.slug),
|
||||||
|
]);
|
||||||
@@ -125,7 +125,7 @@ export const httpProxy = async (
|
|||||||
const params = _u.searchParams;
|
const params = _u.searchParams;
|
||||||
const isDownload = params.get('download') === 'true';
|
const isDownload = params.get('download') === 'true';
|
||||||
if (proxyUrl.startsWith(minioResources)) {
|
if (proxyUrl.startsWith(minioResources)) {
|
||||||
console.log('isMinio', proxyUrl)
|
// console.log('isMinio', proxyUrl)
|
||||||
const isOk = await minioProxy(req, res, { ...opts, isDownload });
|
const isOk = await minioProxy(req, res, { ...opts, isDownload });
|
||||||
if (!isOk) {
|
if (!isOk) {
|
||||||
userApp.clearCacheData();
|
userApp.clearCacheData();
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { omit } from 'es-toolkit';
|
|||||||
import { IncomingMessage, ServerResponse } from 'http';
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
import { renderServerHtml } from '../html/render-server-html.ts';
|
import { renderServerHtml } from '../html/render-server-html.ts';
|
||||||
import { baseURL } from '../domain.ts';
|
import { baseURL } from '../domain.ts';
|
||||||
|
import { app } from '@/app.ts'
|
||||||
|
import { N5Service } from '@/routes/n5-link/modules/n5.services.ts';
|
||||||
type ProxyOptions = {
|
type ProxyOptions = {
|
||||||
createNotFoundPage: (msg?: string) => any;
|
createNotFoundPage: (msg?: string) => any;
|
||||||
};
|
};
|
||||||
@@ -18,19 +19,30 @@ export const N5Proxy = async (req: IncomingMessage, res: ServerResponse, opts?:
|
|||||||
opts?.createNotFoundPage?.('应用未找到');
|
opts?.createNotFoundPage?.('应用未找到');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const convexResult = await convex.query(convexApi.nCode.getBySlug, { slug: app })
|
let n5Data = null;
|
||||||
if (!convexResult) {
|
let data = null;
|
||||||
opts?.createNotFoundPage?.('应用未找到');
|
let link = '';
|
||||||
return false;
|
try {
|
||||||
}
|
const convexResult = await N5Service.getBySlug(app);
|
||||||
const data = convexResult.data ?? {};
|
if (!convexResult || convexResult.length === 0) {
|
||||||
const link = data.link;
|
opts?.createNotFoundPage?.('应用未找到');
|
||||||
if (!link) {
|
return false;
|
||||||
opts?.createNotFoundPage?.('不存在对应的跳转的链接');
|
}
|
||||||
|
n5Data = convexResult[0];
|
||||||
|
data = n5Data.data ?? {};
|
||||||
|
link = data.link;
|
||||||
|
if (!link) {
|
||||||
|
opts?.createNotFoundPage?.('不存在对应的跳转的链接');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error fetching N5 data:', e);
|
||||||
|
opts?.createNotFoundPage?.('应用数据异常');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.useOwnerToken) {
|
if (data?.useOwnerToken) {
|
||||||
const userId = convexResult.userId;
|
const userId = n5Data.userId;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
opts?.createNotFoundPage?.('未绑定账号');
|
opts?.createNotFoundPage?.('未绑定账号');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ export class UserApp {
|
|||||||
const key = 'user:app:set:' + app + ':' + user;
|
const key = 'user:app:set:' + app + ':' + user;
|
||||||
const value = await redis.hget(key, appFileUrl);
|
const value = await redis.hget(key, appFileUrl);
|
||||||
// const values = await redis.hgetall(key);
|
// const values = await redis.hgetall(key);
|
||||||
// console.log('getFile', values);
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
static async getDomainApp(domain: string) {
|
static async getDomainApp(domain: string) {
|
||||||
|
|||||||
@@ -150,6 +150,11 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
const isDev = isLocalhost(dns?.hostName);
|
const isDev = isLocalhost(dns?.hostName);
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
console.debug('开发环境访问:', req.url, 'Host:', dns.hostName);
|
console.debug('开发环境访问:', req.url, 'Host:', dns.hostName);
|
||||||
|
if (req.url === '/') {
|
||||||
|
res.writeHead(302, { Location: '/root/router-studio/' });
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isIpv4OrIpv6(dns.hostName)) {
|
if (isIpv4OrIpv6(dns.hostName)) {
|
||||||
// 打印出 req.url 和错误信息
|
// 打印出 req.url 和错误信息
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ import './views/index.ts';
|
|||||||
import './query-views/index.ts';
|
import './query-views/index.ts';
|
||||||
|
|
||||||
import './flowme/index.ts'
|
import './flowme/index.ts'
|
||||||
|
|
||||||
|
import './n5-link/index.ts'
|
||||||
1
src/routes/n5-link/index.ts
Normal file
1
src/routes/n5-link/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './list.ts';
|
||||||
3
src/routes/n5-link/list.ts
Normal file
3
src/routes/n5-link/list.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import './short-link.ts';
|
||||||
|
import './n5-make/index.ts';
|
||||||
|
import './n5-shop/index.ts';
|
||||||
7
src/routes/n5-link/modules/n5.services.ts
Normal file
7
src/routes/n5-link/modules/n5.services.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
export class N5Service {
|
||||||
|
static async getBySlug(slug: string) {
|
||||||
|
return db.select().from(schema.shortLink).where(eq(schema.shortLink.slug, slug)).limit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/routes/n5-link/n5-make/create.ts
Normal file
24
src/routes/n5-link/n5-make/create.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'create',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `创建 Make, 参数:
|
||||||
|
slug: 唯一业务ID, 必填
|
||||||
|
resources: 资源列表(数组), 选填
|
||||||
|
`,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { userId, createdAt, updatedAt, id: _id, ...rest } = ctx.query.data || {};
|
||||||
|
if (!rest.slug) {
|
||||||
|
ctx.throw(400, 'slug 参数缺失');
|
||||||
|
}
|
||||||
|
const result = await db.insert(schema.n5Make).values({
|
||||||
|
...rest,
|
||||||
|
userId: tokenUser.id,
|
||||||
|
}).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
25
src/routes/n5-link/n5-make/delete.ts
Normal file
25
src/routes/n5-link/n5-make/delete.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'delete',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '删除 Make, 参数: id Make ID',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.n5Make).where(eq(schema.n5Make.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, 'Make 不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限删除该 Make');
|
||||||
|
}
|
||||||
|
await db.delete(schema.n5Make).where(eq(schema.n5Make.id, id));
|
||||||
|
ctx.body = { success: true };
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
4
src/routes/n5-link/n5-make/index.ts
Normal file
4
src/routes/n5-link/n5-make/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import './list.ts';
|
||||||
|
import './create.ts';
|
||||||
|
import './update.ts';
|
||||||
|
import './delete.ts';
|
||||||
48
src/routes/n5-link/n5-make/list.ts
Normal file
48
src/routes/n5-link/n5-make/list.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { desc, eq, count } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'list',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '获取 Make 列表, 参数: page, pageSize, sort',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const uid = tokenUser.id;
|
||||||
|
const { page = 1, pageSize = 20, sort = 'DESC' } = ctx.query || {};
|
||||||
|
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const orderByField = sort === 'ASC' ? schema.n5Make.updatedAt : desc(schema.n5Make.updatedAt);
|
||||||
|
|
||||||
|
const whereCondition = eq(schema.n5Make.userId, uid);
|
||||||
|
const [list, totalCount] = await Promise.all([
|
||||||
|
db.select().from(schema.n5Make).where(whereCondition).limit(pageSize).offset(offset).orderBy(orderByField),
|
||||||
|
db.select({ count: count() }).from(schema.n5Make).where(whereCondition),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
pagination: { page, current: page, pageSize, total: totalCount[0]?.count || 0 },
|
||||||
|
};
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 获取单个
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'get',
|
||||||
|
description: '获取单个 Make, 参数: id 或 slug',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { id, slug } = ctx.query || {};
|
||||||
|
if (!id && !slug) {
|
||||||
|
ctx.throw(400, 'id 或 slug 参数缺失');
|
||||||
|
}
|
||||||
|
const condition = id ? eq(schema.n5Make.id, id) : eq(schema.n5Make.slug, slug);
|
||||||
|
const existing = await db.select().from(schema.n5Make).where(condition).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, 'Make 不存在');
|
||||||
|
}
|
||||||
|
ctx.body = existing[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
29
src/routes/n5-link/n5-make/update.ts
Normal file
29
src/routes/n5-link/n5-make/update.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `更新 Make, 参数:
|
||||||
|
id: Make ID, 必填
|
||||||
|
slug: 唯一业务ID, 选填
|
||||||
|
resources: 资源列表(数组), 选填
|
||||||
|
`,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id, userId, createdAt, updatedAt, ...rest } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.n5Make).where(eq(schema.n5Make.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, 'Make 不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限更新该 Make');
|
||||||
|
}
|
||||||
|
const result = await db.update(schema.n5Make).set({ ...rest }).where(eq(schema.n5Make.id, id)).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
32
src/routes/n5-link/n5-shop/create.ts
Normal file
32
src/routes/n5-link/n5-shop/create.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'create',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `创建商品, 参数:
|
||||||
|
slug: 唯一业务ID, 必填
|
||||||
|
title: 商品标题, 必填
|
||||||
|
platform: 商品平台, 必填
|
||||||
|
orderLink: 订单链接, 必填
|
||||||
|
description: 商品描述, 选填
|
||||||
|
link: 商品链接, 选填
|
||||||
|
tags: 标签数组, 选填
|
||||||
|
data: 其他商品信息对象, 选填
|
||||||
|
userinfo: 卖家信息, 选填
|
||||||
|
`,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { userId, createdAt, updatedAt, id: _id, ...rest } = ctx.query.data || {};
|
||||||
|
if (!rest.slug) ctx.throw(400, 'slug 参数缺失');
|
||||||
|
if (!rest.title) ctx.throw(400, 'title 参数缺失');
|
||||||
|
if (!rest.platform) ctx.throw(400, 'platform 参数缺失');
|
||||||
|
if (!rest.orderLink) ctx.throw(400, 'orderLink 参数缺失');
|
||||||
|
const result = await db.insert(schema.n5Shop).values({
|
||||||
|
...rest,
|
||||||
|
userId: tokenUser.id,
|
||||||
|
}).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
25
src/routes/n5-link/n5-shop/delete.ts
Normal file
25
src/routes/n5-link/n5-shop/delete.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'delete',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '删除商品, 参数: id 商品ID',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.n5Shop).where(eq(schema.n5Shop.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '商品不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限删除该商品');
|
||||||
|
}
|
||||||
|
await db.delete(schema.n5Shop).where(eq(schema.n5Shop.id, id));
|
||||||
|
ctx.body = { success: true };
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
4
src/routes/n5-link/n5-shop/index.ts
Normal file
4
src/routes/n5-link/n5-shop/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import './list.ts';
|
||||||
|
import './create.ts';
|
||||||
|
import './update.ts';
|
||||||
|
import './delete.ts';
|
||||||
58
src/routes/n5-link/n5-shop/list.ts
Normal file
58
src/routes/n5-link/n5-shop/list.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'list',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '获取商品列表, 参数: page, pageSize, search, sort',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const uid = tokenUser.id;
|
||||||
|
const { page = 1, pageSize = 20, search, sort = 'DESC' } = ctx.query || {};
|
||||||
|
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const orderByField = sort === 'ASC' ? schema.n5Shop.updatedAt : desc(schema.n5Shop.updatedAt);
|
||||||
|
|
||||||
|
let whereCondition: any = eq(schema.n5Shop.userId, uid);
|
||||||
|
if (search) {
|
||||||
|
whereCondition = and(
|
||||||
|
eq(schema.n5Shop.userId, uid),
|
||||||
|
or(
|
||||||
|
like(schema.n5Shop.title, `%${search}%`),
|
||||||
|
like(schema.n5Shop.slug, `%${search}%`),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [list, totalCount] = await Promise.all([
|
||||||
|
db.select().from(schema.n5Shop).where(whereCondition).limit(pageSize).offset(offset).orderBy(orderByField),
|
||||||
|
db.select({ count: count() }).from(schema.n5Shop).where(whereCondition),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
pagination: { page, current: page, pageSize, total: totalCount[0]?.count || 0 },
|
||||||
|
};
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 获取单个
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'get',
|
||||||
|
description: '获取单个商品, 参数: id 或 slug',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { id, slug } = ctx.query || {};
|
||||||
|
if (!id && !slug) {
|
||||||
|
ctx.throw(400, 'id 或 slug 参数缺失');
|
||||||
|
}
|
||||||
|
const condition = id ? eq(schema.n5Shop.id, id) : eq(schema.n5Shop.slug, slug);
|
||||||
|
const existing = await db.select().from(schema.n5Shop).where(condition).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '商品不存在');
|
||||||
|
}
|
||||||
|
ctx.body = existing[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
35
src/routes/n5-link/n5-shop/update.ts
Normal file
35
src/routes/n5-link/n5-shop/update.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `更新商品, 参数:
|
||||||
|
id: 商品ID, 必填
|
||||||
|
title: 商品标题, 选填
|
||||||
|
platform: 商品平台, 选填
|
||||||
|
orderLink: 订单链接, 选填
|
||||||
|
description: 商品描述, 选填
|
||||||
|
link: 商品链接, 选填
|
||||||
|
tags: 标签数组, 选填
|
||||||
|
data: 其他商品信息对象, 选填
|
||||||
|
userinfo: 卖家信息, 选填
|
||||||
|
`,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id, userId, createdAt, updatedAt, ...rest } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.n5Shop).where(eq(schema.n5Shop.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '商品不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限更新该商品');
|
||||||
|
}
|
||||||
|
const result = await db.update(schema.n5Shop).set({ ...rest }).where(eq(schema.n5Shop.id, id)).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
157
src/routes/n5-link/short-link.ts
Normal file
157
src/routes/n5-link/short-link.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'list',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '获取短链列表, 参数: page, pageSize, search, sort',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const uid = tokenUser.id;
|
||||||
|
const { page = 1, pageSize = 20, search, sort = 'DESC' } = ctx.query || {};
|
||||||
|
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const orderByField = sort === 'ASC' ? schema.shortLink.updatedAt : desc(schema.shortLink.updatedAt);
|
||||||
|
|
||||||
|
let whereCondition: any = eq(schema.shortLink.userId, uid);
|
||||||
|
if (search) {
|
||||||
|
whereCondition = and(
|
||||||
|
eq(schema.shortLink.userId, uid),
|
||||||
|
or(
|
||||||
|
like(schema.shortLink.title, `%${search}%`),
|
||||||
|
like(schema.shortLink.slug, `%${search}%`),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [list, totalCount] = await Promise.all([
|
||||||
|
db.select().from(schema.shortLink).where(whereCondition).limit(pageSize).offset(offset).orderBy(orderByField),
|
||||||
|
db.select({ count: count() }).from(schema.shortLink).where(whereCondition),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
pagination: { page, current: page, pageSize, total: totalCount[0]?.count || 0 },
|
||||||
|
};
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 获取单个
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'get',
|
||||||
|
description: '获取单个短链, 参数: id 或 slug',
|
||||||
|
middleware: ['auth'],
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { id, slug } = ctx.query || {};
|
||||||
|
if (!id && !slug) {
|
||||||
|
ctx.throw(400, 'id 或 slug 参数缺失');
|
||||||
|
}
|
||||||
|
const condition = id ? eq(schema.shortLink.id, id) : eq(schema.shortLink.slug, slug);
|
||||||
|
const existing = await db.select().from(schema.shortLink).where(condition).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '短链不存在');
|
||||||
|
}
|
||||||
|
ctx.body = existing[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 创建
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'create',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `创建短链`,
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
slug: z.string().optional().describe('对外唯一业务ID'),
|
||||||
|
title: z.string().optional().describe('标题'),
|
||||||
|
description: z.string().optional().describe('描述'),
|
||||||
|
tags: z.array(z.string()).optional().describe('标签数组'),
|
||||||
|
data: z.record(z.string(), z.any()).optional().describe('附加数据对象'),
|
||||||
|
type: z.enum(['link', 'agent']).optional().describe('类型'),
|
||||||
|
version: z.string().optional().describe('版本号'),
|
||||||
|
}).describe('创建短链参数对象'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { userId, createdAt, updatedAt, id: _id, ...rest } = ctx.query.data || {};
|
||||||
|
if (!rest.slug) {
|
||||||
|
ctx.throw(400, 'slug 参数缺失');
|
||||||
|
}
|
||||||
|
const code = rest.code || nanoid(8);
|
||||||
|
const result = await db.insert(schema.shortLink).values({
|
||||||
|
...rest,
|
||||||
|
code,
|
||||||
|
userId: tokenUser.id,
|
||||||
|
}).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `更新短链`,
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional().describe('短链ID'),
|
||||||
|
title: z.string().optional().describe('标题'),
|
||||||
|
description: z.string().optional().describe('描述'),
|
||||||
|
tags: z.array(z.string()).optional().describe('标签数组'),
|
||||||
|
data: z.record(z.string(), z.any()).optional().describe('附加数据对象'),
|
||||||
|
type: z.enum(['link', 'agent']).optional().describe('类型'),
|
||||||
|
version: z.string().optional().describe('版本号'),
|
||||||
|
}).describe('更新短链参数对象'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id, userId, createdAt, updatedAt, ...rest } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.shortLink).where(eq(schema.shortLink.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '短链不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限更新该短链');
|
||||||
|
}
|
||||||
|
const result = await db.update(schema.shortLink).set({ ...rest }).where(eq(schema.shortLink.id, id)).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'delete',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '删除短链, 参数: id 短链ID',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.shortLink).where(eq(schema.shortLink.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '短链不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限删除该短链');
|
||||||
|
}
|
||||||
|
await db.delete(schema.shortLink).where(eq(schema.shortLink.id, id));
|
||||||
|
ctx.body = { success: true };
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
@@ -15,5 +15,6 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
|
"scripts/**/*"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user