From 8529fd15751b2950412dcffbe68db4e69645e03d Mon Sep 17 00:00:00 2001
From: 18631081161 <2088094923@qq.com>
Date: Sat, 20 Dec 2025 23:19:40 +0800
Subject: [PATCH] =?UTF-8?q?=E9=A2=84=E7=BA=A6=E8=A7=84=E5=88=99.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
admin/src/views/config/index.vue | 121 +++++++++++++
admin/src/views/services/index.vue | 36 ----
backend/src/app.js | 6 +
.../controllers/adminBookingRuleController.js | 166 ++++++++++++++++++
.../src/controllers/bookingRuleController.js | 35 ++++
backend/src/models/BookingRule.js | 42 +++++
backend/src/models/index.js | 2 +
backend/src/routes/adminBookingRuleRoutes.js | 46 +++++
backend/src/routes/bookingRuleRoutes.js | 13 ++
miniprogram/src/mixins/bookingRulesMixin.js | 31 ++++
miniprogram/src/modules/api/AppServer.js | 14 ++
.../appointment/airfare-info-entry-page.vue | 36 +++-
.../appointment/hotel-reservation-page.vue | 36 +++-
13 files changed, 546 insertions(+), 38 deletions(-)
create mode 100644 backend/src/controllers/adminBookingRuleController.js
create mode 100644 backend/src/controllers/bookingRuleController.js
create mode 100644 backend/src/models/BookingRule.js
create mode 100644 backend/src/routes/adminBookingRuleRoutes.js
create mode 100644 backend/src/routes/bookingRuleRoutes.js
create mode 100644 miniprogram/src/mixins/bookingRulesMixin.js
diff --git a/admin/src/views/config/index.vue b/admin/src/views/config/index.vue
index 256b1aa..0af8416 100644
--- a/admin/src/views/config/index.vue
+++ b/admin/src/views/config/index.vue
@@ -256,6 +256,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存此规则
+
+
+
+
+
+
+
+
+
+ 保存全部规则
+
+
+
+
+
@@ -281,6 +323,31 @@ const contactFormRef = ref(null)
const agreementFormRef = ref(null)
const inviteFormRef = ref(null)
+// 预约登记规则相关
+const activeBookingRules = ref([])
+const savingRule = ref('')
+const bookingRulesConfig = ref({})
+
+// 服务类型选项
+const serviceTypeOptions = [
+ { value: 'flight', label: '全球机票代理' },
+ { value: 'hotel', label: '全球酒店预定' },
+ { value: 'lounge', label: '全球机场贵宾室服务' },
+ { value: 'airport_transfer', label: '机场接/送机服务' },
+ { value: 'unaccompanied_minor', label: '无成人陪伴儿童代办' },
+ { value: 'train', label: '高铁票代订' },
+ { value: 'telemedicine', label: '远程医疗问诊代理服务' },
+ { value: 'special_passenger', label: '特殊(特护)旅客定制服务代办' },
+ { value: 'pet_transport', label: '宠物托运代理' },
+ { value: 'guide_translation', label: '西班牙语专业导游/翻译服务' },
+ { value: 'visa', label: '签证咨询' },
+ { value: 'exhibition', label: '墨西哥展会咨询与协办服务' },
+ { value: 'air_logistics', label: '跨境航空物流/快递一站式服务' },
+ { value: 'sea_freight', label: '海运/清关一站式服务' },
+ { value: 'travel_planning', label: '旅游线路规划/咨询' },
+ { value: 'insurance', label: '跨境出行意外保险/国际财产保险咨询' }
+]
+
// Original configs for reset
const originalConfigs = reactive({
appearance: {},
@@ -431,6 +498,9 @@ const loadConfigs = async () => {
}
})
}
+
+ // 加载预约规则(使用独立接口)
+ await loadBookingRules()
} catch (error) {
console.error('Load configs error:', error)
ElMessage.error('加载配置失败')
@@ -445,6 +515,21 @@ const refreshConfigs = async () => {
ElMessage.success('配置已刷新')
}
+// 加载预约规则
+const loadBookingRules = async () => {
+ try {
+ const response = await api.get('/api/v1/admin/booking-rules')
+ if (response.data.code === 0) {
+ const rules = response.data.data || []
+ rules.forEach(rule => {
+ bookingRulesConfig.value[rule.serviceType] = rule.rules || ''
+ })
+ }
+ } catch (error) {
+ console.error('Load booking rules error:', error)
+ }
+}
+
// Save appearance config
const saveAppearanceConfig = async () => {
if (!appearanceFormRef.value) return
@@ -629,6 +714,40 @@ const resetInviteConfig = () => {
inviteFormRef.value?.clearValidate()
}
+// 保存单个预约规则
+const saveBookingRule = async (serviceType) => {
+ savingRule.value = serviceType
+ try {
+ await api.put(`/api/v1/admin/booking-rules/${serviceType}`, {
+ rules: bookingRulesConfig.value[serviceType] || ''
+ })
+ ElMessage.success('规则保存成功')
+ } catch (error) {
+ console.error('Save booking rule error:', error)
+ ElMessage.error(error.response?.data?.message || '保存规则失败')
+ } finally {
+ savingRule.value = ''
+ }
+}
+
+// 保存全部预约规则
+const saveAllBookingRules = async () => {
+ saving.value = true
+ try {
+ const rules = serviceTypeOptions.map(item => ({
+ serviceType: item.value,
+ rules: bookingRulesConfig.value[item.value] || ''
+ }))
+ await api.post('/api/v1/admin/booking-rules/batch', { rules })
+ ElMessage.success('全部规则保存成功')
+ } catch (error) {
+ console.error('Save all booking rules error:', error)
+ ElMessage.error(error.response?.data?.message || '保存规则失败')
+ } finally {
+ saving.value = false
+ }
+}
+
// Clear images
const clearLogo = async () => {
try {
@@ -741,6 +860,8 @@ const handleTabChange = (tabName) => {
contactFormRef.value?.clearValidate()
} else if (tabName === 'agreement') {
agreementFormRef.value?.clearValidate()
+ } else if (tabName === 'booking_rules') {
+ // 预约登记规则tab不需要清除验证
}
}
diff --git a/admin/src/views/services/index.vue b/admin/src/views/services/index.vue
index 925d7f0..3c26746 100644
--- a/admin/src/views/services/index.vue
+++ b/admin/src/views/services/index.vue
@@ -176,33 +176,6 @@
- 多语言描述
-
-
-
-
-
-
-
-
-
-
-
其他信息
@@ -313,9 +286,6 @@ const defaultForm = {
titleZh: '',
titleEn: '',
titlePt: '',
- descriptionZh: '',
- descriptionEn: '',
- descriptionPt: '',
image: '',
sortOrder: 0,
status: 'active'
@@ -439,9 +409,6 @@ function handleEdit(row) {
titleZh: row.titleZh,
titleEn: row.titleEn,
titlePt: row.titlePt,
- descriptionZh: row.descriptionZh || '',
- descriptionEn: row.descriptionEn || '',
- descriptionPt: row.descriptionPt || '',
image: row.image || '',
sortOrder: row.sortOrder || 0,
status: row.status
@@ -491,9 +458,6 @@ async function handleSubmit() {
try {
const data = { ...form }
// Convert empty strings to null for optional fields
- if (!data.descriptionZh) data.descriptionZh = null
- if (!data.descriptionEn) data.descriptionEn = null
- if (!data.descriptionPt) data.descriptionPt = null
if (!data.image) data.image = null
if (data.price === null || data.price === '') data.price = null
diff --git a/backend/src/app.js b/backend/src/app.js
index a77d72a..d8173cd 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -130,6 +130,9 @@ const createApp = () => {
const homeRoutes = require('./routes/homeRoutes');
app.use('/api/v1/home', homeRoutes);
+ const bookingRuleRoutes = require('./routes/bookingRuleRoutes');
+ app.use('/api/v1/booking-rules', bookingRuleRoutes);
+
const adminConfigRoutes = require('./routes/adminConfigRoutes');
app.use('/api/v1/admin/config', adminConfigRoutes);
@@ -148,6 +151,9 @@ const createApp = () => {
const adminCommissionRoutes = require('./routes/adminCommissionRoutes');
app.use('/api/v1/admin/commissions', adminCommissionRoutes);
+ const adminBookingRuleRoutes = require('./routes/adminBookingRuleRoutes');
+ app.use('/api/v1/admin/booking-rules', adminBookingRuleRoutes);
+
// 404 handler
app.use(notFoundHandler);
diff --git a/backend/src/controllers/adminBookingRuleController.js b/backend/src/controllers/adminBookingRuleController.js
new file mode 100644
index 0000000..f481946
--- /dev/null
+++ b/backend/src/controllers/adminBookingRuleController.js
@@ -0,0 +1,166 @@
+const { BookingRule } = require('../models');
+
+/**
+ * 获取所有预约规则
+ */
+const getAllRules = async (req, res) => {
+ try {
+ const rules = await BookingRule.findAll({
+ order: [['createdAt', 'ASC']]
+ });
+
+ res.json({
+ code: 0,
+ message: 'success',
+ data: rules
+ });
+ } catch (error) {
+ console.error('Get all booking rules error:', error);
+ res.status(500).json({
+ code: 500,
+ message: '获取预约规则失败',
+ data: null
+ });
+ }
+};
+
+/**
+ * 根据服务类型获取规则
+ */
+const getRuleByServiceType = async (req, res) => {
+ try {
+ const { serviceType } = req.params;
+
+ const rule = await BookingRule.findOne({
+ where: { serviceType }
+ });
+
+ res.json({
+ code: 0,
+ message: 'success',
+ data: rule
+ });
+ } catch (error) {
+ console.error('Get booking rule error:', error);
+ res.status(500).json({
+ code: 500,
+ message: '获取预约规则失败',
+ data: null
+ });
+ }
+};
+
+/**
+ * 创建或更新预约规则
+ */
+const upsertRule = async (req, res) => {
+ try {
+ const { serviceType } = req.params;
+ const { rules, status } = req.body;
+
+ const [rule, created] = await BookingRule.upsert({
+ serviceType,
+ rules: rules || '',
+ status: status || 'active'
+ }, {
+ returning: true
+ });
+
+ res.json({
+ code: 0,
+ message: created ? '创建成功' : '更新成功',
+ data: rule
+ });
+ } catch (error) {
+ console.error('Upsert booking rule error:', error);
+ res.status(500).json({
+ code: 500,
+ message: '保存预约规则失败',
+ data: null
+ });
+ }
+};
+
+/**
+ * 批量更新预约规则
+ */
+const batchUpsertRules = async (req, res) => {
+ try {
+ const { rules } = req.body; // [{ serviceType, rules, status }]
+
+ if (!Array.isArray(rules)) {
+ return res.status(400).json({
+ code: 400,
+ message: '参数格式错误',
+ data: null
+ });
+ }
+
+ const results = [];
+ for (const item of rules) {
+ const [rule] = await BookingRule.upsert({
+ serviceType: item.serviceType,
+ rules: item.rules || '',
+ status: item.status || 'active'
+ }, {
+ returning: true
+ });
+ results.push(rule);
+ }
+
+ res.json({
+ code: 0,
+ message: '批量保存成功',
+ data: results
+ });
+ } catch (error) {
+ console.error('Batch upsert booking rules error:', error);
+ res.status(500).json({
+ code: 500,
+ message: '批量保存预约规则失败',
+ data: null
+ });
+ }
+};
+
+/**
+ * 删除预约规则
+ */
+const deleteRule = async (req, res) => {
+ try {
+ const { serviceType } = req.params;
+
+ const deleted = await BookingRule.destroy({
+ where: { serviceType }
+ });
+
+ if (deleted) {
+ res.json({
+ code: 0,
+ message: '删除成功',
+ data: null
+ });
+ } else {
+ res.status(404).json({
+ code: 404,
+ message: '规则不存在',
+ data: null
+ });
+ }
+ } catch (error) {
+ console.error('Delete booking rule error:', error);
+ res.status(500).json({
+ code: 500,
+ message: '删除预约规则失败',
+ data: null
+ });
+ }
+};
+
+module.exports = {
+ getAllRules,
+ getRuleByServiceType,
+ upsertRule,
+ batchUpsertRules,
+ deleteRule
+};
diff --git a/backend/src/controllers/bookingRuleController.js b/backend/src/controllers/bookingRuleController.js
new file mode 100644
index 0000000..8259152
--- /dev/null
+++ b/backend/src/controllers/bookingRuleController.js
@@ -0,0 +1,35 @@
+const { BookingRule } = require('../models');
+
+/**
+ * 根据服务类型获取预约规则(公开接口)
+ */
+const getRuleByServiceType = async (req, res) => {
+ try {
+ const { serviceType } = req.params;
+
+ const rule = await BookingRule.findOne({
+ where: {
+ serviceType,
+ status: 'active'
+ },
+ attributes: ['serviceType', 'rules']
+ });
+
+ res.json({
+ code: 0,
+ message: 'success',
+ data: rule ? { rules: rule.rules } : { rules: '' }
+ });
+ } catch (error) {
+ console.error('Get booking rule error:', error);
+ res.status(500).json({
+ code: 500,
+ message: '获取预约规则失败',
+ data: null
+ });
+ }
+};
+
+module.exports = {
+ getRuleByServiceType
+};
diff --git a/backend/src/models/BookingRule.js b/backend/src/models/BookingRule.js
new file mode 100644
index 0000000..899ab7b
--- /dev/null
+++ b/backend/src/models/BookingRule.js
@@ -0,0 +1,42 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+/**
+ * BookingRule Model
+ * 预约登记规则配置,每个服务类型单独配置
+ */
+const BookingRule = sequelize.define('BookingRule', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ serviceType: {
+ type: DataTypes.STRING(50),
+ allowNull: false,
+ unique: true,
+ field: 'service_type',
+ comment: '服务类型标识',
+ },
+ rules: {
+ type: DataTypes.TEXT,
+ allowNull: true,
+ comment: '预约登记规则内容',
+ },
+ status: {
+ type: DataTypes.ENUM('active', 'inactive'),
+ defaultValue: 'active',
+ allowNull: false,
+ comment: '状态',
+ },
+}, {
+ tableName: 'booking_rules',
+ timestamps: true,
+ underscored: true,
+ indexes: [
+ { fields: ['service_type'], unique: true },
+ { fields: ['status'] },
+ ],
+});
+
+module.exports = BookingRule;
diff --git a/backend/src/models/index.js b/backend/src/models/index.js
index f6a029f..080a644 100644
--- a/backend/src/models/index.js
+++ b/backend/src/models/index.js
@@ -13,6 +13,7 @@ const Config = require('./Config');
const HotService = require('./HotService');
const PaymentOrder = require('./PaymentOrder');
const Commission = require('./Commission');
+const BookingRule = require('./BookingRule');
/**
* Define model associations
@@ -102,5 +103,6 @@ module.exports = {
HotService,
PaymentOrder,
Commission,
+ BookingRule,
syncDatabase,
};
diff --git a/backend/src/routes/adminBookingRuleRoutes.js b/backend/src/routes/adminBookingRuleRoutes.js
new file mode 100644
index 0000000..59cc7f9
--- /dev/null
+++ b/backend/src/routes/adminBookingRuleRoutes.js
@@ -0,0 +1,46 @@
+const express = require('express');
+const adminBookingRuleController = require('../controllers/adminBookingRuleController');
+const { authenticateAdmin } = require('../middleware/auth');
+const { requireAdmin } = require('../middleware/rbac');
+
+const router = express.Router();
+
+// Apply authentication middleware
+router.use(authenticateAdmin);
+
+/**
+ * @route GET /api/v1/admin/booking-rules
+ * @desc 获取所有预约规则
+ * @access Private (Admin)
+ */
+router.get('/', requireAdmin, adminBookingRuleController.getAllRules);
+
+/**
+ * @route GET /api/v1/admin/booking-rules/:serviceType
+ * @desc 根据服务类型获取规则
+ * @access Private (Admin)
+ */
+router.get('/:serviceType', requireAdmin, adminBookingRuleController.getRuleByServiceType);
+
+/**
+ * @route PUT /api/v1/admin/booking-rules/:serviceType
+ * @desc 创建或更新预约规则
+ * @access Private (Admin)
+ */
+router.put('/:serviceType', requireAdmin, adminBookingRuleController.upsertRule);
+
+/**
+ * @route POST /api/v1/admin/booking-rules/batch
+ * @desc 批量更新预约规则
+ * @access Private (Admin)
+ */
+router.post('/batch', requireAdmin, adminBookingRuleController.batchUpsertRules);
+
+/**
+ * @route DELETE /api/v1/admin/booking-rules/:serviceType
+ * @desc 删除预约规则
+ * @access Private (Admin)
+ */
+router.delete('/:serviceType', requireAdmin, adminBookingRuleController.deleteRule);
+
+module.exports = router;
diff --git a/backend/src/routes/bookingRuleRoutes.js b/backend/src/routes/bookingRuleRoutes.js
new file mode 100644
index 0000000..9d3b55c
--- /dev/null
+++ b/backend/src/routes/bookingRuleRoutes.js
@@ -0,0 +1,13 @@
+const express = require('express');
+const bookingRuleController = require('../controllers/bookingRuleController');
+
+const router = express.Router();
+
+/**
+ * @route GET /api/v1/booking-rules/:serviceType
+ * @desc 根据服务类型获取预约规则(公开接口)
+ * @access Public
+ */
+router.get('/:serviceType', bookingRuleController.getRuleByServiceType);
+
+module.exports = router;
diff --git a/miniprogram/src/mixins/bookingRulesMixin.js b/miniprogram/src/mixins/bookingRulesMixin.js
new file mode 100644
index 0000000..451dd34
--- /dev/null
+++ b/miniprogram/src/mixins/bookingRulesMixin.js
@@ -0,0 +1,31 @@
+/**
+ * 预约登记规则混入
+ * 用于在预约表单页面加载和显示预约规则
+ */
+import { AppServer } from '@/modules/api/AppServer.js'
+
+const appServer = new AppServer()
+
+export default {
+ data() {
+ return {
+ bookingRules: '' // 预约登记规则
+ }
+ },
+ methods: {
+ /**
+ * 加载预约登记规则
+ * @param {String} serviceType - 服务类型
+ */
+ async loadBookingRules(serviceType) {
+ try {
+ const result = await appServer.GetBookingRule(serviceType)
+ if (result.code === 0 && result.data && result.data.rules) {
+ this.bookingRules = result.data.rules
+ }
+ } catch (error) {
+ console.error('加载预约规则失败:', error)
+ }
+ }
+ }
+}
diff --git a/miniprogram/src/modules/api/AppServer.js b/miniprogram/src/modules/api/AppServer.js
index 2668948..3558335 100644
--- a/miniprogram/src/modules/api/AppServer.js
+++ b/miniprogram/src/modules/api/AppServer.js
@@ -66,6 +66,9 @@ serverConfig.apiUrl_Upload_Image = baseUrl + '/api/v1/upload/image' // 上传图
// ==================== 小程序码相关接口 ====================
serverConfig.apiUrl_QRCode_GetMiniProgram = baseUrl + '/api/v1/qrcode/miniprogram' // 获取小程序码
+// ==================== 预约规则相关接口 ====================
+serverConfig.apiUrl_BookingRule_Get = baseUrl + '/api/v1/booking-rules' // 获取预约规则
+
/**
* 获取完整的application/x-www-form-urlencoded请求参数
@@ -610,6 +613,17 @@ AppServer.prototype.GetMiniProgramQRCode = async function(params = {}) {
})
}
+/**
+ * 获取预约登记规则
+ * @param {String} serviceType - 服务类型
+ */
+AppServer.prototype.GetBookingRule = async function(serviceType) {
+ var url = serverConfig.apiUrl_BookingRule_Get + '/' + serviceType
+ return this.getData(url).then((data) => {
+ return data;
+ })
+}
+
/**
* 上传图片
* @param {String} filePath - 本地文件路径
diff --git a/miniprogram/src/pages/appointment/airfare-info-entry-page.vue b/miniprogram/src/pages/appointment/airfare-info-entry-page.vue
index c68efcf..38f1ed4 100644
--- a/miniprogram/src/pages/appointment/airfare-info-entry-page.vue
+++ b/miniprogram/src/pages/appointment/airfare-info-entry-page.vue
@@ -12,7 +12,12 @@
-
+
+ 预约登记规则
+ {{ bookingRules }}
+
+
@@ -238,9 +243,11 @@