diff --git a/backend/services/admin-service/src/pre-planting/pre-planting-config.controller.ts b/backend/services/admin-service/src/pre-planting/pre-planting-config.controller.ts index acaf0851..2998050e 100644 --- a/backend/services/admin-service/src/pre-planting/pre-planting-config.controller.ts +++ b/backend/services/admin-service/src/pre-planting/pre-planting-config.controller.ts @@ -27,6 +27,11 @@ class TogglePrePlantingConfigDto { isActive: boolean; } +class UpdatePrePlantingAgreementDto { + @IsString() + text: string; +} + @ApiTags('预种计划配置') @Controller('admin/pre-planting') export class PrePlantingConfigController { @@ -124,6 +129,26 @@ export class PrePlantingConfigController { async getStats() { return this.proxyService.getStats(); } + + // ============================================ + // [2026-02-28] 新增:预种协议管理 + // ============================================ + + @Get('agreement') + @ApiOperation({ summary: '获取预种协议文本' }) + @ApiResponse({ status: HttpStatus.OK, description: '协议文本' }) + async getAgreement() { + const text = await this.configService.getAgreement(); + return { text }; + } + + @Put('agreement') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '更新预种协议文本' }) + @ApiResponse({ status: HttpStatus.OK, description: '更新成功' }) + async updateAgreement(@Body() dto: UpdatePrePlantingAgreementDto) { + return this.configService.updateAgreement(dto.text); + } } /** @@ -137,8 +162,10 @@ export class PublicPrePlantingConfigController { ) {} @Get('config') - @ApiOperation({ summary: '获取预种计划开关状态(内部API)' }) + @ApiOperation({ summary: '获取预种计划开关状态(内部API,含协议文本)' }) async getConfig() { - return this.configService.getConfig(); + const config = await this.configService.getConfig(); + const agreementText = await this.configService.getAgreement(); + return { ...config, agreementText }; } } diff --git a/backend/services/admin-service/src/pre-planting/pre-planting-config.service.ts b/backend/services/admin-service/src/pre-planting/pre-planting-config.service.ts index 0228729a..498f5889 100644 --- a/backend/services/admin-service/src/pre-planting/pre-planting-config.service.ts +++ b/backend/services/admin-service/src/pre-planting/pre-planting-config.service.ts @@ -25,6 +25,38 @@ export class PrePlantingConfigService { }; } + /** + * 获取预种协议文本(从 system_configs 表读取) + */ + async getAgreement(): Promise { + const config = await this.prisma.systemConfig.findUnique({ + where: { key: 'pre_planting_agreement' }, + }); + return config?.value ?? null; + } + + /** + * 更新预种协议文本(upsert 到 system_configs 表) + */ + async updateAgreement(text: string, updatedBy?: string): Promise<{ text: string }> { + await this.prisma.systemConfig.upsert({ + where: { key: 'pre_planting_agreement' }, + create: { + key: 'pre_planting_agreement', + value: text, + description: '预种计划购买协议文本', + updatedBy: updatedBy || null, + }, + update: { + value: text, + updatedBy: updatedBy || null, + }, + }); + + this.logger.log(`[PRE-PLANTING] Agreement text updated by ${updatedBy || 'unknown'}`); + return { text }; + } + async updateConfig( isActive: boolean, updatedBy?: string, diff --git a/backend/services/planting-service/src/pre-planting/api/controllers/pre-planting-public.controller.ts b/backend/services/planting-service/src/pre-planting/api/controllers/pre-planting-public.controller.ts new file mode 100644 index 00000000..a7523294 --- /dev/null +++ b/backend/services/planting-service/src/pre-planting/api/controllers/pre-planting-public.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, HttpStatus } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { PrePlantingAdminClient } from '../../infrastructure/external/pre-planting-admin.client'; + +/** + * [2026-02-28] 预种计划公开 API(无需 JWT) + * + * 供 mobile-app 在未登录或登录前获取预种计划配置信息(开关状态 + 协议文本)。 + * 与 PrePlantingController(需 JWT)分离,避免在类级 @UseGuards 中打洞。 + */ +@ApiTags('预种计划-公开') +@Controller('pre-planting') +export class PrePlantingPublicController { + constructor( + private readonly adminClient: PrePlantingAdminClient, + ) {} + + @Get('config') + @ApiOperation({ summary: '获取预种计划配置(公开,含协议文本)' }) + @ApiResponse({ status: HttpStatus.OK, description: '配置信息' }) + async getConfig() { + return this.adminClient.getPrePlantingConfig(); + } +} diff --git a/backend/services/planting-service/src/pre-planting/infrastructure/external/pre-planting-admin.client.ts b/backend/services/planting-service/src/pre-planting/infrastructure/external/pre-planting-admin.client.ts index 75e26834..5ea0c250 100644 --- a/backend/services/planting-service/src/pre-planting/infrastructure/external/pre-planting-admin.client.ts +++ b/backend/services/planting-service/src/pre-planting/infrastructure/external/pre-planting-admin.client.ts @@ -6,6 +6,7 @@ import { firstValueFrom } from 'rxjs'; export interface PrePlantingConfig { isActive: boolean; activatedAt: Date | null; + agreementText?: string | null; } @Injectable() diff --git a/backend/services/planting-service/src/pre-planting/pre-planting.module.ts b/backend/services/planting-service/src/pre-planting/pre-planting.module.ts index 00cdc9b5..abdc87cd 100644 --- a/backend/services/planting-service/src/pre-planting/pre-planting.module.ts +++ b/backend/services/planting-service/src/pre-planting/pre-planting.module.ts @@ -4,6 +4,7 @@ import { HttpModule } from '@nestjs/axios'; // Controllers import { PrePlantingController } from './api/controllers/pre-planting.controller'; import { InternalPrePlantingController } from './api/controllers/internal-pre-planting.controller'; +import { PrePlantingPublicController } from './api/controllers/pre-planting-public.controller'; // Application Services import { PrePlantingApplicationService } from './application/services/pre-planting-application.service'; @@ -55,6 +56,7 @@ import { PrePlantingAuthorizationClient } from './infrastructure/external/pre-pl ], controllers: [ PrePlantingController, + PrePlantingPublicController, InternalPrePlantingController, ], providers: [ diff --git a/frontend/admin-web/src/app/(dashboard)/pre-planting/page.tsx b/frontend/admin-web/src/app/(dashboard)/pre-planting/page.tsx index 361d4a3b..4d20e1db 100644 --- a/frontend/admin-web/src/app/(dashboard)/pre-planting/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/pre-planting/page.tsx @@ -20,11 +20,12 @@ * 侧边栏入口在"数据统计"和"系统维护"之间。 */ -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/common'; import { PageContainer } from '@/components/layout'; import { cn } from '@/utils/helpers'; import { formatDateTime } from '@/utils/formatters'; +import { prePlantingService } from '@/services/prePlantingService'; import { usePrePlantingConfig, usePrePlantingStats, @@ -88,6 +89,36 @@ export default function PrePlantingPage() { keyword: activeTab === 'merges' ? keyword : undefined, }); + // === 协议文本管理 === + const [agreementText, setAgreementText] = useState(''); + const [agreementSaving, setAgreementSaving] = useState(false); + const [agreementMsg, setAgreementMsg] = useState(''); + + const loadAgreement = useCallback(async () => { + try { + const res = await prePlantingService.getAgreement(); + setAgreementText(res.text ?? ''); + } catch { + // 未配置时使用空文本 + } + }, []); + + useEffect(() => { loadAgreement(); }, [loadAgreement]); + + const handleSaveAgreement = async () => { + setAgreementSaving(true); + setAgreementMsg(''); + try { + await prePlantingService.updateAgreement(agreementText); + setAgreementMsg('协议已保存'); + setTimeout(() => setAgreementMsg(''), 3000); + } catch { + setAgreementMsg('保存失败,请重试'); + } finally { + setAgreementSaving(false); + } + }; + // === 开关切换 === const handleToggle = () => { if (!config || toggleConfig.isPending) return; @@ -163,6 +194,49 @@ export default function PrePlantingPage() { + {/* 预种协议管理 */} +
+
+

预种协议管理

+
+ {agreementMsg && ( + + {agreementMsg} + + )} + +
+
+