From c75d62051695ca25e4a730ca83989a93778e404a Mon Sep 17 00:00:00 2001 From: tikkhun Date: Sat, 22 Nov 2025 10:30:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=AB=99=E7=82=B9):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加站点实体、服务层和控制器,支持站点的CRUD操作 同步配置中的站点信息到数据库 提供站点禁用/启用功能 --- src/config/config.default.ts | 2 + src/configuration.ts | 7 +++ src/controller/site.controller.ts | 80 ++++++++++++++++++++++++------ src/dto/site.dto.ts | 62 ++++++++++++++++++++--- src/entity/site.entity.ts | 28 +++++++++++ src/service/site.service.ts | 81 +++++++++++++++++++++++++++++++ 6 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 src/entity/site.entity.ts create mode 100644 src/service/site.service.ts diff --git a/src/config/config.default.ts b/src/config/config.default.ts index fb7e085..fd99d08 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -33,6 +33,7 @@ import { Customer } from '../entity/customer.entity'; import { DeviceWhitelist } from '../entity/device_whitelist'; import { AuthCode } from '../entity/auth_code'; import { Subscription } from '../entity/subscription.entity'; +import { Site } from '../entity/site.entity'; export default { // use for cookie sign key, should change to your own and keep security @@ -74,6 +75,7 @@ export default { DeviceWhitelist, AuthCode, Subscription, + Site, ], synchronize: true, logging: false, diff --git a/src/configuration.ts b/src/configuration.ts index 8205c4b..e413b12 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -17,6 +17,7 @@ import * as crossDomain from '@midwayjs/cross-domain'; import * as cron from '@midwayjs/cron'; import * as jwt from '@midwayjs/jwt'; import { USER_KEY } from './decorator/user.decorator'; +import { SiteService } from './service/site.service'; import { AuthMiddleware } from './middleware/auth.middleware'; @Configuration({ @@ -45,6 +46,9 @@ export class MainConfiguration { @Inject() jwtService: jwt.JwtService; // 注入 JwtService 实例 + @Inject() + siteService: SiteService; + async onReady() { // add middleware this.app.useMiddleware([ReportMiddleware, AuthMiddleware]); @@ -74,5 +78,8 @@ export class MainConfiguration { } } ); + + const sites = this.app.getConfig('wpSite') || []; + await this.siteService.syncFromConfig(sites); } } diff --git a/src/controller/site.controller.ts b/src/controller/site.controller.ts index c84c7c2..5c848d5 100644 --- a/src/controller/site.controller.ts +++ b/src/controller/site.controller.ts @@ -1,25 +1,75 @@ -import { Config, Controller, Get } from '@midwayjs/core'; +import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core'; import { ApiOkResponse } from '@midwayjs/swagger'; import { WpSitesResponse } from '../dto/reponse.dto'; -import { successResponse } from '../utils/response.util'; -import { WpSite } from '../interface'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { SiteService } from '../service/site.service'; +import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto'; @Controller('/site') export class SiteController { - @Config('wpSite') - sites: WpSite[]; + @Inject() + siteService: SiteService; - @ApiOkResponse({ - description: '关联网站', - type: WpSitesResponse, - }) + @ApiOkResponse({ description: '关联网站', type: WpSitesResponse }) @Get('/all') async all() { - return successResponse( - this.sites.map(v => ({ - id: v.id, - siteName: v.siteName, - })) - ); + try { + const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false }); + return successResponse(items.map((v: any) => ({ id: v.id, siteName: v.siteName }))); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @Post('/create') + async create(@Body() body: CreateSiteDTO) { + try { + await this.siteService.create(body); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @Put('/update/:id') + async update(@Param('id') id: string, @Body() body: UpdateSiteDTO) { + try { + await this.siteService.update(Number(id), body); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } + } + + @Get('/get/:id') + async get(@Param('id') id: string) { + try { + const data = await this.siteService.get(Number(id), false); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @Get('/list') + async list(@Query() query: QuerySiteDTO) { + try { + const data = await this.siteService.list(query, false); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + // 批量查询改为使用 /site/list?ids=1,2,3 + + @Put('/disable/:id') + async disable(@Param('id') id: string, @Body() body: DisableSiteDTO) { + try { + await this.siteService.disable(Number(id), body.disabled); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } } } diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index 5c31a27..17fcdd1 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -8,7 +8,7 @@ export class SiteConfig { @ApiProperty({ description: '站点 URL' }) @Rule(RuleType.string()) - wpApiUrl: string; + apiUrl: string; @ApiProperty({ description: '站点 rest key' }) @Rule(RuleType.string()) @@ -22,11 +22,61 @@ export class SiteConfig { @Rule(RuleType.string()) siteName: string; - @ApiProperty({ description: '站点邮箱' }) - @Rule(RuleType.string()) - email?: string; + @ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] }) + @Rule(RuleType.string().valid('woocommerce', 'shopyy')) + type: string; - @ApiProperty({ description: '站点邮箱密码' }) + @ApiProperty({ description: 'SKU 前缀' }) @Rule(RuleType.string()) - emailPswd?: string; + skuPrefix: string; +} + +export class CreateSiteDTO { + @Rule(RuleType.string().optional()) + apiUrl?: string; + @Rule(RuleType.string().optional()) + consumerKey?: string; + @Rule(RuleType.string().optional()) + consumerSecret?: string; + @Rule(RuleType.string()) + siteName: string; + @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) + type?: string; + @Rule(RuleType.string().optional()) + skuPrefix?: string; +} + +export class UpdateSiteDTO { + @Rule(RuleType.string().optional()) + apiUrl?: string; + @Rule(RuleType.string().optional()) + consumerKey?: string; + @Rule(RuleType.string().optional()) + consumerSecret?: string; + @Rule(RuleType.string().optional()) + siteName?: string; + @Rule(RuleType.boolean().optional()) + isDisabled?: boolean; + @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) + type?: string; + @Rule(RuleType.string().optional()) + skuPrefix?: string; +} + +export class QuerySiteDTO { + @Rule(RuleType.number().optional()) + current?: number; + @Rule(RuleType.number().optional()) + pageSize?: number; + @Rule(RuleType.string().optional()) + keyword?: string; + @Rule(RuleType.boolean().optional()) + isDisabled?: boolean; + @Rule(RuleType.string().optional()) + ids?: string; +} + +export class DisableSiteDTO { + @Rule(RuleType.boolean()) + disabled: boolean; } diff --git a/src/entity/site.entity.ts b/src/entity/site.entity.ts new file mode 100644 index 0000000..d4e3215 --- /dev/null +++ b/src/entity/site.entity.ts @@ -0,0 +1,28 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('site') +export class Site { + @PrimaryGeneratedColumn({ type: 'int' }) + id: number; + + @Column({ type: 'varchar', length: 255, nullable: true }) + apiUrl: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + consumerKey: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + consumerSecret: string; + + @Column({ type: 'varchar', length: 255, unique: true }) + siteName: string; + + @Column({ type: 'varchar', length: 32, default: 'woocommerce' }) + type: string; // 平台类型:woocommerce | shopyy + + @Column({ type: 'varchar', length: 64, nullable: true }) + skuPrefix: string; + + @Column({ type: 'tinyint', default: 0 }) + isDisabled: number; +} \ No newline at end of file diff --git a/src/service/site.service.ts b/src/service/site.service.ts new file mode 100644 index 0000000..8510865 --- /dev/null +++ b/src/service/site.service.ts @@ -0,0 +1,81 @@ +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository, Like, In } from 'typeorm'; +import { Site } from '../entity/site.entity'; +import { WpSite } from '../interface'; +import { UpdateSiteDTO } from '../dto/site.dto'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class SiteService { + @InjectEntityModel(Site) + siteModel: Repository; + + async syncFromConfig(sites: WpSite[] = []) { + for (const s of sites) { + const exist = await this.siteModel.findOne({ where: { siteName: s.siteName } }); + const payload: Partial = { + siteName: s.siteName, + apiUrl: (s as any).wpApiUrl, + consumerKey: (s as any).consumerKey, + consumerSecret: (s as any).consumerSecret, + type: 'woocommerce', + }; + if (exist) await this.siteModel.update({ id: exist.id }, payload); + else await this.siteModel.insert(payload as Site); + } + } + + async create(data: Partial) { + await this.siteModel.insert(data as Site); + return true; + } + + async update(id: string | number, data: UpdateSiteDTO) { + const payload: Partial = { + ...data, + isDisabled: + data.isDisabled === undefined + ? undefined + : data.isDisabled + ? 1 + : 0, + } as any; + await this.siteModel.update({ id: Number(id) }, payload); + return true; + } + + async get(id: string | number, includeSecret = false) { + const s = await this.siteModel.findOne({ where: { id: Number(id) } }); + if (!s) return null; + if (includeSecret) return s; + const { consumerKey, consumerSecret, emailPswd, ...rest } = s as any; + return rest; + } + + async list(param: { current?: number; pageSize?: number; keyword?: string; isDisabled?: boolean; ids?: string }, includeSecret = false) { + const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any; + const where: any = {}; + if (keyword) where.siteName = Like(`%${keyword}%`); + if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0; + if (ids) { + const numIds = String(ids) + .split(',') + .filter(Boolean) + .map((i) => Number(i)) + .filter((v) => !Number.isNaN(v)); + if (numIds.length > 0) where.id = In(numIds); + } + const [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize }); + const data = includeSecret ? items : items.map((s: any) => { + const { consumerKey, consumerSecret, ...rest } = s; + return rest; + }); + return { items: data, total, current, pageSize }; + } + + async disable(id: string | number, disabled: boolean) { + await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled ? 1 : 0 }); + return true; + } +} \ No newline at end of file