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 @@