From 48425c6120749d07ca09c7d0f27edbb2778ff2c8 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Tue, 10 Mar 2026 19:46:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=9F=AD=E9=93=BE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E3=80=81=E6=9B=B4=E6=96=B0=E3=80=81=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=92=8C=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + scripts/import-data.ts | 78 ++++++++++ scripts/ncode-list.json | 104 ++++++++++++++ src/db/schema.ts | 4 +- src/db/schemas/n-code-schema.ts | 57 ++++++++ src/modules/fm-manager/proxy/http-proxy.ts | 2 +- src/modules/n5/index.ts | 34 +++-- src/modules/user-app/index.ts | 1 - src/routes-simple/page-proxy.ts | 5 + src/routes/index.ts | 4 +- src/routes/n5-link/index.ts | 1 + src/routes/n5-link/list.ts | 3 + src/routes/n5-link/modules/n5.services.ts | 7 + src/routes/n5-link/n5-make/create.ts | 24 ++++ src/routes/n5-link/n5-make/delete.ts | 25 ++++ src/routes/n5-link/n5-make/index.ts | 4 + src/routes/n5-link/n5-make/list.ts | 48 +++++++ src/routes/n5-link/n5-make/update.ts | 29 ++++ src/routes/n5-link/n5-shop/create.ts | 32 +++++ src/routes/n5-link/n5-shop/delete.ts | 25 ++++ src/routes/n5-link/n5-shop/index.ts | 4 + src/routes/n5-link/n5-shop/list.ts | 58 ++++++++ src/routes/n5-link/n5-shop/update.ts | 35 +++++ src/routes/n5-link/short-link.ts | 157 +++++++++++++++++++++ tsconfig.json | 1 + 25 files changed, 728 insertions(+), 15 deletions(-) create mode 100644 scripts/import-data.ts create mode 100644 scripts/ncode-list.json create mode 100644 src/db/schemas/n-code-schema.ts create mode 100644 src/routes/n5-link/index.ts create mode 100644 src/routes/n5-link/list.ts create mode 100644 src/routes/n5-link/modules/n5.services.ts create mode 100644 src/routes/n5-link/n5-make/create.ts create mode 100644 src/routes/n5-link/n5-make/delete.ts create mode 100644 src/routes/n5-link/n5-make/index.ts create mode 100644 src/routes/n5-link/n5-make/list.ts create mode 100644 src/routes/n5-link/n5-make/update.ts create mode 100644 src/routes/n5-link/n5-shop/create.ts create mode 100644 src/routes/n5-link/n5-shop/delete.ts create mode 100644 src/routes/n5-link/n5-shop/index.ts create mode 100644 src/routes/n5-link/n5-shop/list.ts create mode 100644 src/routes/n5-link/n5-shop/update.ts create mode 100644 src/routes/n5-link/short-link.ts diff --git a/package.json b/package.json index 16abd54..c6caec5 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "pub:kevisual": "npm run build && npm run deploy:kevisual && npm run reload:kevisual", "start": "pm2 start 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", "drizzle:migrate": "npx drizzle-kit migrate", "drizzle:push": "npx drizzle-kit push", diff --git a/scripts/import-data.ts b/scripts/import-data.ts new file mode 100644 index 0000000..d5ade2c --- /dev/null +++ b/scripts/import-data.ts @@ -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; + 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); +}); diff --git a/scripts/ncode-list.json b/scripts/ncode-list.json new file mode 100644 index 0000000..fe49422 --- /dev/null +++ b/scripts/ncode-list.json @@ -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" + } +] \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 1e41f9e..5d69235 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,3 +1,5 @@ import { pgTable, uuid, jsonb, timestamp, text } from "drizzle-orm/pg-core"; import { InferSelectModel, InferInsertModel, desc } from "drizzle-orm"; -export * from './drizzle/schema.ts'; \ No newline at end of file +export * from './drizzle/schema.ts'; + +export * from './schemas/n-code-schema.ts' \ No newline at end of file diff --git a/src/db/schemas/n-code-schema.ts b/src/db/schemas/n-code-schema.ts new file mode 100644 index 0000000..1fe249e --- /dev/null +++ b/src/db/schemas/n-code-schema.ts @@ -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), +]); \ No newline at end of file diff --git a/src/modules/fm-manager/proxy/http-proxy.ts b/src/modules/fm-manager/proxy/http-proxy.ts index ba939f5..d0cf38f 100644 --- a/src/modules/fm-manager/proxy/http-proxy.ts +++ b/src/modules/fm-manager/proxy/http-proxy.ts @@ -125,7 +125,7 @@ export const httpProxy = async ( const params = _u.searchParams; const isDownload = params.get('download') === 'true'; if (proxyUrl.startsWith(minioResources)) { - console.log('isMinio', proxyUrl) + // console.log('isMinio', proxyUrl) const isOk = await minioProxy(req, res, { ...opts, isDownload }); if (!isOk) { userApp.clearCacheData(); diff --git a/src/modules/n5/index.ts b/src/modules/n5/index.ts index d7a5806..00b36b4 100644 --- a/src/modules/n5/index.ts +++ b/src/modules/n5/index.ts @@ -4,7 +4,8 @@ import { omit } from 'es-toolkit'; import { IncomingMessage, ServerResponse } from 'http'; import { renderServerHtml } from '../html/render-server-html.ts'; import { baseURL } from '../domain.ts'; - +import { app } from '@/app.ts' +import { N5Service } from '@/routes/n5-link/modules/n5.services.ts'; type ProxyOptions = { createNotFoundPage: (msg?: string) => any; }; @@ -18,19 +19,30 @@ export const N5Proxy = async (req: IncomingMessage, res: ServerResponse, opts?: opts?.createNotFoundPage?.('应用未找到'); return false; } - const convexResult = await convex.query(convexApi.nCode.getBySlug, { slug: app }) - if (!convexResult) { - opts?.createNotFoundPage?.('应用未找到'); - return false; - } - const data = convexResult.data ?? {}; - const link = data.link; - if (!link) { - opts?.createNotFoundPage?.('不存在对应的跳转的链接'); + let n5Data = null; + let data = null; + let link = ''; + try { + const convexResult = await N5Service.getBySlug(app); + if (!convexResult || convexResult.length === 0) { + opts?.createNotFoundPage?.('应用未找到'); + return false; + } + 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; } + if (data?.useOwnerToken) { - const userId = convexResult.userId; + const userId = n5Data.userId; if (!userId) { opts?.createNotFoundPage?.('未绑定账号'); return false; diff --git a/src/modules/user-app/index.ts b/src/modules/user-app/index.ts index a287d14..eac4eab 100644 --- a/src/modules/user-app/index.ts +++ b/src/modules/user-app/index.ts @@ -110,7 +110,6 @@ export class UserApp { const key = 'user:app:set:' + app + ':' + user; const value = await redis.hget(key, appFileUrl); // const values = await redis.hgetall(key); - // console.log('getFile', values); return value; } static async getDomainApp(domain: string) { diff --git a/src/routes-simple/page-proxy.ts b/src/routes-simple/page-proxy.ts index a873b0c..b7d1d11 100644 --- a/src/routes-simple/page-proxy.ts +++ b/src/routes-simple/page-proxy.ts @@ -150,6 +150,11 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR const isDev = isLocalhost(dns?.hostName); if (isDev) { console.debug('开发环境访问:', req.url, 'Host:', dns.hostName); + if (req.url === '/') { + res.writeHead(302, { Location: '/root/router-studio/' }); + res.end(); + return; + } } else { if (isIpv4OrIpv6(dns.hostName)) { // 打印出 req.url 和错误信息 diff --git a/src/routes/index.ts b/src/routes/index.ts index 539f435..f262388 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -20,4 +20,6 @@ import './views/index.ts'; import './query-views/index.ts'; -import './flowme/index.ts' \ No newline at end of file +import './flowme/index.ts' + +import './n5-link/index.ts' \ No newline at end of file diff --git a/src/routes/n5-link/index.ts b/src/routes/n5-link/index.ts new file mode 100644 index 0000000..366cb31 --- /dev/null +++ b/src/routes/n5-link/index.ts @@ -0,0 +1 @@ +import './list.ts'; \ No newline at end of file diff --git a/src/routes/n5-link/list.ts b/src/routes/n5-link/list.ts new file mode 100644 index 0000000..46f6abe --- /dev/null +++ b/src/routes/n5-link/list.ts @@ -0,0 +1,3 @@ +import './short-link.ts'; +import './n5-make/index.ts'; +import './n5-shop/index.ts'; diff --git a/src/routes/n5-link/modules/n5.services.ts b/src/routes/n5-link/modules/n5.services.ts new file mode 100644 index 0000000..1083231 --- /dev/null +++ b/src/routes/n5-link/modules/n5.services.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/routes/n5-link/n5-make/create.ts b/src/routes/n5-link/n5-make/create.ts new file mode 100644 index 0000000..f580b8d --- /dev/null +++ b/src/routes/n5-link/n5-make/create.ts @@ -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); diff --git a/src/routes/n5-link/n5-make/delete.ts b/src/routes/n5-link/n5-make/delete.ts new file mode 100644 index 0000000..83dcf4f --- /dev/null +++ b/src/routes/n5-link/n5-make/delete.ts @@ -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); diff --git a/src/routes/n5-link/n5-make/index.ts b/src/routes/n5-link/n5-make/index.ts new file mode 100644 index 0000000..2257bff --- /dev/null +++ b/src/routes/n5-link/n5-make/index.ts @@ -0,0 +1,4 @@ +import './list.ts'; +import './create.ts'; +import './update.ts'; +import './delete.ts'; diff --git a/src/routes/n5-link/n5-make/list.ts b/src/routes/n5-link/n5-make/list.ts new file mode 100644 index 0000000..51ce4fa --- /dev/null +++ b/src/routes/n5-link/n5-make/list.ts @@ -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); diff --git a/src/routes/n5-link/n5-make/update.ts b/src/routes/n5-link/n5-make/update.ts new file mode 100644 index 0000000..1b840a4 --- /dev/null +++ b/src/routes/n5-link/n5-make/update.ts @@ -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); diff --git a/src/routes/n5-link/n5-shop/create.ts b/src/routes/n5-link/n5-shop/create.ts new file mode 100644 index 0000000..bb3d769 --- /dev/null +++ b/src/routes/n5-link/n5-shop/create.ts @@ -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); diff --git a/src/routes/n5-link/n5-shop/delete.ts b/src/routes/n5-link/n5-shop/delete.ts new file mode 100644 index 0000000..59aa4ac --- /dev/null +++ b/src/routes/n5-link/n5-shop/delete.ts @@ -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); diff --git a/src/routes/n5-link/n5-shop/index.ts b/src/routes/n5-link/n5-shop/index.ts new file mode 100644 index 0000000..2257bff --- /dev/null +++ b/src/routes/n5-link/n5-shop/index.ts @@ -0,0 +1,4 @@ +import './list.ts'; +import './create.ts'; +import './update.ts'; +import './delete.ts'; diff --git a/src/routes/n5-link/n5-shop/list.ts b/src/routes/n5-link/n5-shop/list.ts new file mode 100644 index 0000000..b977dce --- /dev/null +++ b/src/routes/n5-link/n5-shop/list.ts @@ -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); diff --git a/src/routes/n5-link/n5-shop/update.ts b/src/routes/n5-link/n5-shop/update.ts new file mode 100644 index 0000000..2636880 --- /dev/null +++ b/src/routes/n5-link/n5-shop/update.ts @@ -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); diff --git a/src/routes/n5-link/short-link.ts b/src/routes/n5-link/short-link.ts new file mode 100644 index 0000000..769fd62 --- /dev/null +++ b/src/routes/n5-link/short-link.ts @@ -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); diff --git a/tsconfig.json b/tsconfig.json index 9ff73a1..11d6612 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,6 @@ }, "include": [ "src/**/*", + "scripts/**/*" ], } \ No newline at end of file