From 11bfc151418f31b2c6739dc9be4678aa0f688c56 Mon Sep 17 00:00:00 2001 From: gouki Date: Thu, 6 Aug 2026 12:26:29 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AE=B8=E6=84=BF?= =?UTF-8?q?=E6=A0=91=E5=8A=9F=E8=83=BD=E5=92=8CGo=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增许愿树页面(Canvas绘制、许愿条展示) - 新增许愿详情页面 - 新增许愿创建弹窗(免费/付费两种模式) - 新增网络请求封装 utils/request.js - 新增Go后端项目结构(Gin + Inertia.js) - 新增用户、订单、许愿数据模型 - 新增数据库迁移脚本 - 更新.gitignore补全忽略规则 - 更新.env.local配置(robot=2, 版本1.0.1) - 添加secrets目录说明文档 --- .gitignore | 85 +++++++ mini/app.json | 10 +- mini/images/wish-active.png | Bin 0 -> 67 bytes mini/images/wish.png | Bin 0 -> 67 bytes mini/pages/wish-detail/wish-detail.js | 56 +++++ mini/pages/wish-detail/wish-detail.json | 4 + mini/pages/wish-detail/wish-detail.wxml | 46 ++++ mini/pages/wish-detail/wish-detail.wxss | 145 +++++++++++ mini/pages/wish-tree/wish-tree.js | 274 ++++++++++++++++++++ mini/pages/wish-tree/wish-tree.json | 5 + mini/pages/wish-tree/wish-tree.wxml | 117 +++++++++ mini/pages/wish-tree/wish-tree.wxss | 307 +++++++++++++++++++++++ mini/utils/request.js | 67 +++++ server/README.md | 96 +++++++ server/cmd/main.go | 94 +++++++ server/go.mod | 41 +++ server/internal/config/config.go | 82 ++++++ server/internal/handler/admin.go | 42 ++++ server/internal/handler/order.go | 43 ++++ server/internal/handler/pay.go | 39 +++ server/internal/handler/user.go | 58 +++++ server/internal/handler/wish.go | 57 +++++ server/internal/middleware/middleware.go | 46 ++++ server/internal/model/order.go | 32 +++ server/internal/model/user.go | 34 +++ server/internal/model/wish.go | 41 +++ server/internal/service/user.go | 26 ++ server/internal/service/wish.go | 38 +++ server/migrations/001_init.sql | 104 ++++++++ server/scripts/build.sh | 24 ++ server/scripts/dev.sh | 13 + 31 files changed, 2025 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 mini/images/wish-active.png create mode 100644 mini/images/wish.png create mode 100644 mini/pages/wish-detail/wish-detail.js create mode 100644 mini/pages/wish-detail/wish-detail.json create mode 100644 mini/pages/wish-detail/wish-detail.wxml create mode 100644 mini/pages/wish-detail/wish-detail.wxss create mode 100644 mini/pages/wish-tree/wish-tree.js create mode 100644 mini/pages/wish-tree/wish-tree.json create mode 100644 mini/pages/wish-tree/wish-tree.wxml create mode 100644 mini/pages/wish-tree/wish-tree.wxss create mode 100644 mini/utils/request.js create mode 100644 server/README.md create mode 100644 server/cmd/main.go create mode 100644 server/go.mod create mode 100644 server/internal/config/config.go create mode 100644 server/internal/handler/admin.go create mode 100644 server/internal/handler/order.go create mode 100644 server/internal/handler/pay.go create mode 100644 server/internal/handler/user.go create mode 100644 server/internal/handler/wish.go create mode 100644 server/internal/middleware/middleware.go create mode 100644 server/internal/model/order.go create mode 100644 server/internal/model/user.go create mode 100644 server/internal/model/wish.go create mode 100644 server/internal/service/user.go create mode 100644 server/internal/service/wish.go create mode 100644 server/migrations/001_init.sql create mode 100644 server/scripts/build.sh create mode 100644 server/scripts/dev.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8d0e66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +# 环境配置 +.env.local +.env.*.local + +# 密钥和证书 +secrets/ +*.key +*.pem +*.p12 +*.mobileprovision + +# 依赖 +node_modules/ +mini/node_modules/ + +# 构建输出 +dist/ +build/ +*.min.js +*.min.wxss + +# 小程序 CI +ci-artifacts/ +mini/ci-artifacts/ + +# 编辑器 +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# 系统文件 +.DS_Store +Thumbs.db + +# 日志 +*.log +logs/ + +# 临时文件 +tmp/ +temp/ + +# Go +server/bin/ +server/tmp/ +server/vendor/ +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +# 前端构建(如果 server 包含前端) +server/public/build/ +server/public/hot + +# 数据库 +*.db +*.sqlite +*.sqlite3 + +# 上传文件 +uploads/ +storage/uploads/ + +# 备份 +*.bak +*.backup + +# 压缩包 +*.zip +*.tar.gz +*.rar + +# 微信开发者工具 +.idea/ +*.sublime-* + +# 本地开发 +local/ +.local/ diff --git a/mini/app.json b/mini/app.json index 7972d50..43ca73c 100644 --- a/mini/app.json +++ b/mini/app.json @@ -7,7 +7,9 @@ "pages/fortune/fortune", "pages/divination/divination", "pages/solar-terms/solar-terms", - "pages/settings/settings" + "pages/settings/settings", + "pages/wish-tree/wish-tree", + "pages/wish-detail/wish-detail" ], "window": { "backgroundTextStyle": "light", @@ -46,6 +48,12 @@ "iconPath": "images/fortune.png", "selectedIconPath": "images/fortune-active.png" }, + { + "pagePath": "pages/wish-tree/wish-tree", + "text": "许愿", + "iconPath": "images/wish.png", + "selectedIconPath": "images/wish-active.png" + }, { "pagePath": "pages/settings/settings", "text": "设置", diff --git a/mini/images/wish-active.png b/mini/images/wish-active.png new file mode 100644 index 0000000000000000000000000000000000000000..91a99b94e23a00cc8133f22e3fe2a0b48a015808 GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv6UAQ@H$MqV!6EkIEQ MPgg&ebxsLQ07X&@0{{R3 literal 0 HcmV?d00001 diff --git a/mini/images/wish.png b/mini/images/wish.png new file mode 100644 index 0000000000000000000000000000000000000000..91a99b94e23a00cc8133f22e3fe2a0b48a015808 GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv6UAQ@H$MqV!6EkIEQ MPgg&ebxsLQ07X&@0{{R3 literal 0 HcmV?d00001 diff --git a/mini/pages/wish-detail/wish-detail.js b/mini/pages/wish-detail/wish-detail.js new file mode 100644 index 0000000..deda90f --- /dev/null +++ b/mini/pages/wish-detail/wish-detail.js @@ -0,0 +1,56 @@ +const { request } = require('../../utils/request.js'); + +Page({ + data: { + wish: null, + loading: true, + }, + + onLoad(options) { + const { id } = options; + if (id) { + this.loadWishDetail(id); + } + }, + + async loadWishDetail(id) { + try { + const res = await request({ + url: `/api/wish/detail/${id}`, + method: 'GET', + }); + + if (res.code === 0) { + this.setData({ + wish: res.data, + loading: false, + }); + } + } catch (err) { + console.error('加载许愿详情失败:', err); + // 使用模拟数据 + this.setData({ + wish: { + id: id, + content: '愿家人平安健康,事业顺利,心想事成!', + type: 'paid', + createdAt: '2024-01-15 12:00:00', + user: { + nickname: '祈福用户', + avatar: '', + }, + }, + loading: false, + }); + } + }, + + // 分享 + onShareAppMessage() { + const { wish } = this.data; + return { + title: wish ? `我的愿望:${wish.content.substring(0, 20)}...` : '快来许愿吧!', + path: `/pages/wish-detail/wish-detail?id=${wish ? wish.id : ''}`, + }; + }, +}); diff --git a/mini/pages/wish-detail/wish-detail.json b/mini/pages/wish-detail/wish-detail.json new file mode 100644 index 0000000..280a601 --- /dev/null +++ b/mini/pages/wish-detail/wish-detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "许愿详情", + "enablePullDownRefresh": false +} diff --git a/mini/pages/wish-detail/wish-detail.wxml b/mini/pages/wish-detail/wish-detail.wxml new file mode 100644 index 0000000..850ff19 --- /dev/null +++ b/mini/pages/wish-detail/wish-detail.wxml @@ -0,0 +1,46 @@ + + + 加载中... + + + + + + + + + VIP + + + + + {{wish.content}} + + + + + {{wish.type === 'paid' ? '付费许愿' : '免费许愿'}} + + + + + + + + + + 更多愿望 + + + 愿所有美好如期而至 + + + + + diff --git a/mini/pages/wish-detail/wish-detail.wxss b/mini/pages/wish-detail/wish-detail.wxss new file mode 100644 index 0000000..9278a1c --- /dev/null +++ b/mini/pages/wish-detail/wish-detail.wxss @@ -0,0 +1,145 @@ +.container { + min-height: 100vh; + background: #FFFBF5; + padding: 30rpx; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + height: 400rpx; + color: #999; +} + +.wish-card { + background: #fff; + border-radius: 24rpx; + padding: 40rpx; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.08); +} + +.wish-card.paid { + border: 2rpx solid #FFD700; +} + +.wish-header { + display: flex; + align-items: center; + gap: 20rpx; + margin-bottom: 30rpx; +} + +.avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: #f0f0f0; +} + +.user-info { + flex: 1; +} + +.nickname { + display: block; + font-size: 28rpx; + font-weight: bold; + color: #333; +} + +.time { + display: block; + font-size: 22rpx; + color: #999; + margin-top: 8rpx; +} + +.wish-badge { + background: linear-gradient(135deg, #FFD700, #FFA500); + color: #fff; + font-size: 20rpx; + padding: 8rpx 16rpx; + border-radius: 20rpx; +} + +.wish-content { + padding: 30rpx; + background: #FFF9F0; + border-radius: 16rpx; + margin-bottom: 30rpx; +} + +.wish-content text { + font-size: 32rpx; + color: #333; + line-height: 1.8; +} + +.wish-footer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.type-tag { + font-size: 22rpx; + padding: 8rpx 16rpx; + border-radius: 20rpx; +} + +.type-tag.free { + background: #FFE4E1; + color: #FF6B6B; +} + +.type-tag.paid { + background: #FFF8DC; + color: #DAA520; +} + +.btn-action { + display: flex; + align-items: center; + gap: 8rpx; + background: #C41E3A; + color: #fff; + font-size: 24rpx; + padding: 12rpx 24rpx; + border-radius: 30rpx; + border: none; +} + +.btn-action .icon { + font-size: 28rpx; +} + +/* 推荐区域 */ +.recommend-section { + margin-top: 40rpx; +} + +.section-title { + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; +} + +.recommend-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.recommend-item { + padding: 24rpx; + background: #fff; + border-radius: 16rpx; + box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); +} + +.recommend-text { + font-size: 26rpx; + color: #666; +} diff --git a/mini/pages/wish-tree/wish-tree.js b/mini/pages/wish-tree/wish-tree.js new file mode 100644 index 0000000..8c74ae1 --- /dev/null +++ b/mini/pages/wish-tree/wish-tree.js @@ -0,0 +1,274 @@ +const { request } = require('../../utils/request.js'); + +Page({ + data: { + tree: null, + wishes: [], + loading: true, + canvasWidth: 375, + canvasHeight: 600, + showCreateModal: false, + wishContent: '', + wishType: 'free', // free: 免费, paid: 付费 + maxFreeLength: 20, + maxPaidLength: 100, + products: [], + selectedProduct: null, + }, + + onLoad() { + this.loadWishTree(); + this.loadProducts(); + }, + + onPullDownRefresh() { + this.loadWishTree().then(() => { + wx.stopPullDownRefresh(); + }); + }, + + // 加载许愿树数据 + async loadWishTree() { + try { + const res = await request({ + url: '/api/wish/tree', + method: 'GET', + }); + + if (res.code === 0) { + this.setData({ + tree: res.data.tree, + wishes: res.data.wishes || [], + loading: false, + }); + this.drawTree(); + } + } catch (err) { + console.error('加载许愿树失败:', err); + this.setData({ loading: false }); + // 使用本地模拟数据 + this.setData({ + tree: { id: 1, name: '祈福许愿树', maxWishes: 100 }, + wishes: this.getMockWishes(), + loading: false, + }); + this.drawTree(); + } + }, + + // 加载许愿商品 + async loadProducts() { + try { + const res = await request({ + url: '/api/wish/products', + method: 'GET', + }); + + if (res.code === 0) { + this.setData({ products: res.data.list || [] }); + } + } catch (err) { + console.error('加载商品失败:', err); + // 使用默认商品 + this.setData({ + products: [ + { id: 1, name: '普通许愿条', price: 100, duration: 7, position: 0 }, + { id: 2, name: '精品许愿条', price: 500, duration: 30, position: 10 }, + { id: 3, name: '至尊许愿条', price: 2000, duration: 90, position: 100 }, + ], + }); + } + }, + + // 模拟许愿数据 + getMockWishes() { + return [ + { id: 1, content: '愿家人平安健康', type: 'paid', position: 10, isRobot: false }, + { id: 2, content: '事业顺利', type: 'free', position: 5, isRobot: false }, + { id: 3, content: '心想事成', type: 'paid', position: 20, isRobot: true }, + { id: 4, content: '考试通过', type: 'free', position: 3, isRobot: false }, + { id: 5, content: '财源广进', type: 'paid', position: 15, isRobot: false }, + ]; + }, + + // 绘制许愿树 + drawTree() { + const ctx = wx.createCanvasContext('wishTree', this); + const { canvasWidth, canvasHeight, wishes } = this.data; + + // 清空画布 + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + // 绘制树干 + ctx.setFillStyle('#8B4513'); + ctx.fillRect(canvasWidth / 2 - 20, canvasHeight - 150, 40, 150); + + // 绘制树冠(圆形) + ctx.setFillStyle('#228B22'); + ctx.beginPath(); + ctx.arc(canvasWidth / 2, canvasHeight - 200, 120, 0, 2 * Math.PI); + ctx.fill(); + + // 绘制许愿条 + wishes.forEach((wish, index) => { + const angle = (index / wishes.length) * 2 * Math.PI; + const radius = 80 + Math.random() * 40; + const x = canvasWidth / 2 + radius * Math.cos(angle); + const y = canvasHeight - 200 + radius * Math.sin(angle); + + // 根据类型设置颜色 + if (wish.type === 'paid') { + ctx.setFillStyle('#FFD700'); // 金色 - 付费 + } else { + ctx.setFillStyle('#FF6B6B'); // 红色 - 免费 + } + + // 绘制许愿条(小矩形) + ctx.fillRect(x - 15, y - 5, 30, 10); + + // 绘制文字 + ctx.setFillStyle('#FFFFFF'); + ctx.setFontSize(8); + ctx.setTextAlign('center'); + ctx.fillText(wish.content.substring(0, 4), x, y + 3); + }); + + ctx.draw(); + }, + + // 打开许愿弹窗 + openCreateModal() { + this.setData({ showCreateModal: true }); + }, + + // 关闭许愿弹窗 + closeCreateModal() { + this.setData({ + showCreateModal: false, + wishContent: '', + wishType: 'free', + selectedProduct: null, + }); + }, + + // 输入许愿内容 + onInputContent(e) { + this.setData({ wishContent: e.detail.value }); + }, + + // 选择许愿类型 + selectType(e) { + const type = e.currentTarget.dataset.type; + this.setData({ wishType: type }); + }, + + // 选择商品 + selectProduct(e) { + const id = e.currentTarget.dataset.id; + const product = this.data.products.find(p => p.id === id); + this.setData({ selectedProduct: product }); + }, + + // 提交许愿 + async submitWish() { + const { wishContent, wishType, selectedProduct } = this.data; + + if (!wishContent.trim()) { + wx.showToast({ title: '请输入许愿内容', icon: 'none' }); + return; + } + + // 检查字数限制 + const maxLength = wishType === 'free' ? this.data.maxFreeLength : this.data.maxPaidLength; + if (wishContent.length > maxLength) { + wx.showToast({ title: `最多输入${maxLength}个字`, icon: 'none' }); + return; + } + + // 付费许愿需要选择商品 + if (wishType === 'paid' && !selectedProduct) { + wx.showToast({ title: '请选择许愿商品', icon: 'none' }); + return; + } + + try { + // 如果是付费许愿,先创建订单 + if (wishType === 'paid') { + const orderRes = await request({ + url: '/api/pay/create', + method: 'POST', + data: { + type: 'wish', + productId: selectedProduct.id, + content: wishContent, + }, + }); + + if (orderRes.code === 0) { + // 调起微信支付 + await this.wxPay(orderRes.data); + } + } else { + // 免费许愿直接创建 + const res = await request({ + url: '/api/wish/create', + method: 'POST', + data: { + content: wishContent, + type: 'free', + treeId: this.data.tree.id, + }, + }); + + if (res.code === 0) { + wx.showToast({ title: '许愿成功', icon: 'success' }); + this.closeCreateModal(); + this.loadWishTree(); + } + } + } catch (err) { + console.error('许愿失败:', err); + wx.showToast({ title: '许愿失败,请重试', icon: 'none' }); + } + }, + + // 微信支付 + async wxPay(orderData) { + return new Promise((resolve, reject) => { + wx.requestPayment({ + timeStamp: orderData.timeStamp, + nonceStr: orderData.nonceStr, + package: orderData.package, + signType: orderData.signType, + paySign: orderData.paySign, + success: () => { + wx.showToast({ title: '支付成功', icon: 'success' }); + this.closeCreateModal(); + this.loadWishTree(); + resolve(); + }, + fail: (err) => { + console.error('支付失败:', err); + wx.showToast({ title: '支付失败', icon: 'none' }); + reject(err); + }, + }); + }); + }, + + // 查看许愿详情 + viewWishDetail(e) { + const id = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/wish-detail/wish-detail?id=${id}`, + }); + }, + + // 分享 + onShareAppMessage() { + return { + title: '快来许愿树许下你的愿望吧!', + path: '/pages/wish-tree/wish-tree', + }; + }, +}); diff --git a/mini/pages/wish-tree/wish-tree.json b/mini/pages/wish-tree/wish-tree.json new file mode 100644 index 0000000..961e417 --- /dev/null +++ b/mini/pages/wish-tree/wish-tree.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "许愿树", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/mini/pages/wish-tree/wish-tree.wxml b/mini/pages/wish-tree/wish-tree.wxml new file mode 100644 index 0000000..1f989c8 --- /dev/null +++ b/mini/pages/wish-tree/wish-tree.wxml @@ -0,0 +1,117 @@ + + + + + + + + + {{item.content}} + VIP + + + + + + + + + + + + + 🎋 + 免费许愿:20字以内,可能被覆盖 + + + + 付费许愿:100字以内,优先展示,长期保留 + + + + + + + + + 许下心愿 + × + + + + + + + 免费许愿 + 20字以内 + + + 付费许愿 + 更多特权 + + + + + + + {{wishContent.length}}/{{wishType === 'free' ? maxFreeLength : maxPaidLength}} + + + + + 选择许愿商品 + + + + {{item.name}} + 展示{{item.duration}}天 + + ¥{{item.price / 100}} + + + + + + + + + + + + diff --git a/mini/pages/wish-tree/wish-tree.wxss b/mini/pages/wish-tree/wish-tree.wxss new file mode 100644 index 0000000..1a3e1c7 --- /dev/null +++ b/mini/pages/wish-tree/wish-tree.wxss @@ -0,0 +1,307 @@ +.container { + min-height: 100vh; + background: linear-gradient(180deg, #87CEEB 0%, #E0F6FF 100%); + padding-bottom: 120rpx; +} + +/* 画布容器 */ +.canvas-container { + position: relative; + width: 100%; + height: 800rpx; + overflow: hidden; +} + +.wish-tree-canvas { + width: 100%; + height: 100%; +} + +/* 许愿条覆盖层 */ +.wishes-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.wish-tag { + position: absolute; + padding: 8rpx 16rpx; + border-radius: 20rpx; + font-size: 20rpx; + color: #fff; + pointer-events: auto; + transform: translate(-50%, -50%); + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.2); + max-width: 200rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.wish-tag.free { + background: linear-gradient(135deg, #FF6B6B, #FF8E8E); +} + +.wish-tag.paid { + background: linear-gradient(135deg, #FFD700, #FFA500); +} + +.wish-tag.robot { + border: 2rpx dashed #fff; +} + +.wish-text { + font-size: 20rpx; +} + +.wish-badge { + position: absolute; + top: -8rpx; + right: -8rpx; + background: #FF4500; + color: #fff; + font-size: 16rpx; + padding: 2rpx 8rpx; + border-radius: 10rpx; +} + +/* 操作栏 */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20rpx 30rpx; + background: rgba(255,255,255,0.95); + box-shadow: 0 -2rpx 20rpx rgba(0,0,0,0.1); +} + +.btn-wish { + display: flex; + align-items: center; + justify-content: center; + gap: 10rpx; + background: linear-gradient(135deg, #C41E3A, #E74C3C); + color: #fff; + font-size: 32rpx; + padding: 24rpx; + border-radius: 50rpx; + border: none; +} + +.btn-wish .icon { + font-size: 36rpx; +} + +/* 提示区域 */ +.tips-section { + padding: 30rpx; + margin-top: 20rpx; +} + +.tip-item { + display: flex; + align-items: center; + gap: 16rpx; + padding: 20rpx; + background: rgba(255,255,255,0.8); + border-radius: 16rpx; + margin-bottom: 16rpx; +} + +.tip-icon { + font-size: 36rpx; +} + +.tip-text { + font-size: 26rpx; + color: #666; +} + +/* 弹窗 */ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; +} + +.modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.5); +} + +.modal-content { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: #fff; + border-radius: 32rpx 32rpx 0 0; + max-height: 80vh; + overflow: hidden; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 30rpx; + border-bottom: 1rpx solid #eee; +} + +.modal-title { + font-size: 32rpx; + font-weight: bold; + color: #333; +} + +.modal-close { + font-size: 48rpx; + color: #999; + padding: 10rpx; +} + +.modal-body { + padding: 30rpx; + max-height: 60vh; + overflow-y: auto; +} + +/* 类型选择 */ +.type-selector { + display: flex; + gap: 20rpx; + margin-bottom: 30rpx; +} + +.type-item { + flex: 1; + padding: 24rpx; + border: 2rpx solid #eee; + border-radius: 16rpx; + text-align: center; +} + +.type-item.active { + border-color: #C41E3A; + background: #FFF5F5; +} + +.type-name { + display: block; + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-bottom: 8rpx; +} + +.type-desc { + font-size: 22rpx; + color: #999; +} + +/* 输入框 */ +.wish-input { + width: 100%; + height: 200rpx; + padding: 20rpx; + border: 2rpx solid #eee; + border-radius: 16rpx; + font-size: 28rpx; + box-sizing: border-box; +} + +.input-count { + text-align: right; + font-size: 22rpx; + color: #999; + margin-top: 10rpx; +} + +/* 商品选择 */ +.product-section { + margin-top: 30rpx; +} + +.section-title { + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; +} + +.product-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.product-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 24rpx; + border: 2rpx solid #eee; + border-radius: 16rpx; +} + +.product-item.active { + border-color: #C41E3A; + background: #FFF5F5; +} + +.product-name { + font-size: 28rpx; + font-weight: bold; + color: #333; +} + +.product-desc { + font-size: 22rpx; + color: #999; + margin-top: 8rpx; +} + +.product-price { + font-size: 32rpx; + font-weight: bold; + color: #C41E3A; +} + +/* 底部按钮 */ +.modal-footer { + display: flex; + gap: 20rpx; + padding: 30rpx; + border-top: 1rpx solid #eee; +} + +.modal-footer button { + flex: 1; + padding: 24rpx; + border-radius: 50rpx; + font-size: 28rpx; +} + +.btn-outline { + background: #fff; + border: 2rpx solid #C41E3A; + color: #C41E3A; +} + +.btn-primary { + background: linear-gradient(135deg, #C41E3A, #E74C3C); + color: #fff; + border: none; +} diff --git a/mini/utils/request.js b/mini/utils/request.js new file mode 100644 index 0000000..c14cdf0 --- /dev/null +++ b/mini/utils/request.js @@ -0,0 +1,67 @@ +// 网络请求封装 +const BASE_URL = 'http://localhost:8080'; // 开发环境 + +// 请求拦截器 +function request(options) { + return new Promise((resolve, reject) => { + // 获取 token + const token = wx.getStorageSync('token'); + + wx.request({ + url: BASE_URL + options.url, + method: options.method || 'GET', + data: options.data || {}, + header: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '', + ...options.header, + }, + success: (res) => { + if (res.statusCode === 200) { + resolve(res.data); + } else if (res.statusCode === 401) { + // 未授权,跳转登录 + wx.navigateTo({ url: '/pages/settings/settings' }); + reject(new Error('未授权')); + } else { + reject(new Error(res.data.msg || '请求失败')); + } + }, + fail: (err) => { + console.error('请求失败:', err); + reject(err); + }, + }); + }); +} + +// 上传文件 +function uploadFile(options) { + return new Promise((resolve, reject) => { + const token = wx.getStorageSync('token'); + + wx.uploadFile({ + url: BASE_URL + options.url, + filePath: options.filePath, + name: options.name || 'file', + header: { + 'Authorization': token ? `Bearer ${token}` : '', + }, + formData: options.formData || {}, + success: (res) => { + if (res.statusCode === 200) { + resolve(JSON.parse(res.data)); + } else { + reject(new Error('上传失败')); + } + }, + fail: reject, + }); + }); +} + +module.exports = { + request, + uploadFile, + BASE_URL, +}; diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..6dad13f --- /dev/null +++ b/server/README.md @@ -0,0 +1,96 @@ +# 祈福小助手后端服务 + +基于 Go + Gin + Inertia.js 的后端服务。 + +## 功能模块 + +- **用户模块** — 微信登录、资料修改、授权管理 +- **支付模块** — 微信支付集成、订单管理 +- **许愿树** — 许愿发布、付费许愿、机器人自动许愿 +- **管理后台** — 基于 Inertia.js 的前后端分离管理界面 + +## 技术栈 + +- **后端**: Go 1.22 + Gin + GORM +- **前端**: Inertia.js + React/Vue(待选择) +- **数据库**: MySQL +- **缓存**: Redis + +## 快速开始 + +### 开发环境 + +```bash +# 安装依赖 +go mod download + +# 启动开发服务器 +./scripts/dev.sh +``` + +### 生产构建 + +```bash +# 构建 +./scripts/build.sh + +# 运行 +./bin/lunar-server +``` + +## 项目结构 + +``` +server/ +├── cmd/ # 入口文件 +│ └── main.go +├── internal/ # 内部包 +│ ├── config/ # 配置 +│ ├── handler/ # 处理器 +│ ├── middleware/ # 中间件 +│ ├── model/ # 数据模型 +│ └── service/ # 业务逻辑 +├── web/ # Inertia.js 前端 +│ ├── src/ +│ └── public/ +├── migrations/ # 数据库迁移 +├── scripts/ # 脚本 +└── go.mod +``` + +## API 接口 + +### 用户接口 + +- `POST /api/user/login` — 用户登录 +- `POST /api/user/logout` — 用户登出 +- `GET /api/user/profile` — 获取用户资料 +- `PUT /api/user/profile` — 更新用户资料 +- `POST /api/user/auth` — 微信授权 + +### 支付接口 + +- `POST /api/pay/create` — 创建订单 +- `POST /api/pay/notify` — 支付回调 +- `GET /api/pay/status/:orderId` — 查询支付状态 + +### 订单接口 + +- `GET /api/order/list` — 订单列表 +- `GET /api/order/detail/:id` — 订单详情 +- `POST /api/order/cancel/:id` — 取消订单 + +### 许愿接口 + +- `GET /api/wish/tree` — 获取许愿树 +- `POST /api/wish/create` — 创建许愿 +- `GET /api/wish/list` — 许愿列表 +- `DELETE /api/wish/:id` — 删除许愿 + +## 环境变量 + +参考 `.env.local` 文件配置。 + +## 数据库 + +执行 `migrations/001_init.sql` 初始化数据库。 diff --git a/server/cmd/main.go b/server/cmd/main.go new file mode 100644 index 0000000..017b64d --- /dev/null +++ b/server/cmd/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "log" + "os" + + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + "github.com/gouki/lunar-server/internal/config" + "github.com/gouki/lunar-server/internal/handler" + "github.com/gouki/lunar-server/internal/middleware" +) + +func main() { + // 加载环境变量 + if err := godotenv.Load("../.env.local"); err != nil { + log.Println("No .env.local file found, using system environment") + } + + // 初始化配置 + cfg := config.Load() + + // 设置 Gin 模式 + if cfg.Server.Env == "production" { + gin.SetMode(gin.ReleaseMode) + } + + // 创建路由 + r := gin.Default() + + // 中间件 + r.Use(middleware.CORS()) + r.Use(middleware.Logger()) + + // 静态文件(Inertia.js 前端构建产物) + r.Static("/build", "./web/public/build") + r.LoadHTMLGlob("web/*.html") + + // API 路由 + api := r.Group("/api") + { + // 用户相关 + user := api.Group("/user") + { + user.POST("/login", handler.UserLogin) + user.POST("/logout", handler.UserLogout) + user.GET("/profile", middleware.Auth(), handler.GetUserProfile) + user.PUT("/profile", middleware.Auth(), handler.UpdateUserProfile) + user.POST("/auth", handler.WechatAuth) + } + + // 支付相关 + pay := api.Group("/pay") + { + pay.POST("/create", middleware.Auth(), handler.CreateOrder) + pay.POST("/notify", handler.PayNotify) + pay.GET("/status/:orderId", middleware.Auth(), handler.GetPayStatus) + } + + // 订单管理 + order := api.Group("/order") + { + order.GET("/list", middleware.Auth(), handler.GetOrderList) + order.GET("/detail/:id", middleware.Auth(), handler.GetOrderDetail) + order.POST("/cancel/:id", middleware.Auth(), handler.CancelOrder) + } + + // 许愿树 + wish := api.Group("/wish") + { + wish.GET("/tree", handler.GetWishTree) + wish.POST("/create", middleware.Auth(), handler.CreateWish) + wish.GET("/list", handler.GetWishList) + wish.DELETE("/:id", middleware.Auth(), handler.DeleteWish) + } + } + + // 管理后台(Inertia.js) + admin := r.Group("/admin") + { + admin.GET("/", handler.AdminIndex) + admin.GET("/users", handler.AdminUsers) + admin.GET("/orders", handler.AdminOrders) + admin.GET("/wishes", handler.AdminWishes) + admin.GET("/settings", handler.AdminSettings) + } + + // 启动服务器 + addr := ":" + cfg.Server.Port + log.Printf("Server starting on %s", addr) + if err := r.Run(addr); err != nil { + log.Fatal("Failed to start server:", err) + } +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..61fe28a --- /dev/null +++ b/server/go.mod @@ -0,0 +1,41 @@ +module github.com/gouki/lunar-server + +go 1.22 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/go-inertia/inertia-go v1.0.0 + github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/go-sql-driver/mysql v1.7.1 + github.com/joho/godotenv v1.5.1 + github.com/redis/go-redis/v9 v9.4.0 + golang.org/x/crypto v0.17.0 + gorm.io/gorm v1.25.5 + gorm.io/driver/mysql v1.5.2 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/server/internal/config/config.go b/server/internal/config/config.go new file mode 100644 index 0000000..02dfd70 --- /dev/null +++ b/server/internal/config/config.go @@ -0,0 +1,82 @@ +package config + +import ( + "os" +) + +type Config struct { + Server ServerConfig + Database DatabaseConfig + Redis RedisConfig + JWT JWTConfig + Wechat WechatConfig +} + +type ServerConfig struct { + Port string + Env string +} + +type DatabaseConfig struct { + Host string + Port string + User string + Password string + Name string +} + +type RedisConfig struct { + Host string + Port string + Password string + DB int +} + +type JWTConfig struct { + Secret string +} + +type WechatConfig struct { + AppID string + AppSecret string + PayKey string + MchID string +} + +func Load() *Config { + return &Config{ + Server: ServerConfig{ + Port: getEnv("SERVER_PORT", "8080"), + Env: getEnv("SERVER_ENV", "development"), + }, + Database: DatabaseConfig{ + Host: getEnv("DB_HOST", "localhost"), + Port: getEnv("DB_PORT", "3306"), + User: getEnv("DB_USER", "root"), + Password: getEnv("DB_PASSWORD", ""), + Name: getEnv("DB_NAME", "lunar"), + }, + Redis: RedisConfig{ + Host: getEnv("REDIS_HOST", "localhost"), + Port: getEnv("REDIS_PORT", "6379"), + Password: getEnv("REDIS_PASSWORD", ""), + DB: 0, + }, + JWT: JWTConfig{ + Secret: getEnv("JWT_SECRET", "your-secret-key"), + }, + Wechat: WechatConfig{ + AppID: getEnv("MINI_APP_ID", ""), + AppSecret: getEnv("MINI_APP_SECRET", ""), + PayKey: getEnv("WECHAT_PAY_APIKEY", ""), + MchID: getEnv("WECHAT_PAY_MCHID", ""), + }, + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} diff --git a/server/internal/handler/admin.go b/server/internal/handler/admin.go new file mode 100644 index 0000000..19c4c0c --- /dev/null +++ b/server/internal/handler/admin.go @@ -0,0 +1,42 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// AdminIndex 管理后台首页 +func AdminIndex(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{ + "title": "管理后台", + }) +} + +// AdminUsers 用户管理 +func AdminUsers(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{ + "title": "用户管理", + }) +} + +// AdminOrders 订单管理 +func AdminOrders(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{ + "title": "订单管理", + }) +} + +// AdminWishes 许愿管理 +func AdminWishes(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{ + "title": "许愿管理", + }) +} + +// AdminSettings 系统设置 +func AdminSettings(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{ + "title": "系统设置", + }) +} diff --git a/server/internal/handler/order.go b/server/internal/handler/order.go new file mode 100644 index 0000000..9c336dc --- /dev/null +++ b/server/internal/handler/order.go @@ -0,0 +1,43 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// GetOrderList 获取订单列表 +func GetOrderList(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "list": []interface{}{}, + "total": 0, + }, + }) +} + +// GetOrderDetail 获取订单详情 +func GetOrderDetail(c *gin.Context) { + id := c.Param("id") + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "id": id, + }, + }) +} + +// CancelOrder 取消订单 +func CancelOrder(c *gin.Context) { + id := c.Param("id") + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "id": id, + }, + }) +} diff --git a/server/internal/handler/pay.go b/server/internal/handler/pay.go new file mode 100644 index 0000000..c54d66b --- /dev/null +++ b/server/internal/handler/pay.go @@ -0,0 +1,39 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// CreateOrder 创建订单 +func CreateOrder(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "orderId": "example-order-id", + }, + }) +} + +// PayNotify 支付回调 +func PayNotify(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": "SUCCESS", + "msg": "OK", + }) +} + +// GetPayStatus 获取支付状态 +func GetPayStatus(c *gin.Context) { + orderId := c.Param("orderId") + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "orderId": orderId, + "status": "pending", + }, + }) +} diff --git a/server/internal/handler/user.go b/server/internal/handler/user.go new file mode 100644 index 0000000..6f5f994 --- /dev/null +++ b/server/internal/handler/user.go @@ -0,0 +1,58 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// UserLogin 用户登录 +func UserLogin(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "token": "example-token", + }, + }) +} + +// UserLogout 用户登出 +func UserLogout(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + }) +} + +// GetUserProfile 获取用户资料 +func GetUserProfile(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "id": 1, + "nickname": "用户昵称", + "avatar": "", + }, + }) +} + +// UpdateUserProfile 更新用户资料 +func UpdateUserProfile(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + }) +} + +// WechatAuth 微信授权 +func WechatAuth(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "openid": "example-openid", + }, + }) +} diff --git a/server/internal/handler/wish.go b/server/internal/handler/wish.go new file mode 100644 index 0000000..21c1810 --- /dev/null +++ b/server/internal/handler/wish.go @@ -0,0 +1,57 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// GetWishTree 获取许愿树 +func GetWishTree(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "tree": gin.H{ + "id": 1, + "name": "许愿树", + "wishes": []interface{}{}, + }, + }, + }) +} + +// CreateWish 创建许愿 +func CreateWish(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "id": 1, + }, + }) +} + +// GetWishList 获取许愿列表 +func GetWishList(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "list": []interface{}{}, + "total": 0, + }, + }) +} + +// DeleteWish 删除许愿 +func DeleteWish(c *gin.Context) { + id := c.Param("id") + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": gin.H{ + "id": id, + }, + }) +} diff --git a/server/internal/middleware/middleware.go b/server/internal/middleware/middleware.go new file mode 100644 index 0000000..626809a --- /dev/null +++ b/server/internal/middleware/middleware.go @@ -0,0 +1,46 @@ +package middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// CORS 跨域中间件 +func CORS() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + return + } + + c.Next() + } +} + +// Logger 日志中间件 +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + } +} + +// Auth 认证中间件 +func Auth() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "unauthorized", + }) + c.Abort() + return + } + c.Next() + } +} diff --git a/server/internal/model/order.go b/server/internal/model/order.go new file mode 100644 index 0000000..54fec37 --- /dev/null +++ b/server/internal/model/order.go @@ -0,0 +1,32 @@ +package model + +import ( + "time" +) + +// Order 订单模型 +type Order struct { + ID uint `gorm:"primaryKey" json:"id"` + OrderNo string `gorm:"uniqueIndex;size:32" json:"orderNo"` // 订单号 + UserID uint `gorm:"index" json:"userId"` + Type string `gorm:"size:20" json:"type"` // wish:许愿 vip:会员 + ProductID uint `json:"productId"` + ProductName string `gorm:"size:100" json:"productName"` + Amount int `json:"amount"` // 金额(分) + Status string `gorm:"size:20;default:'pending'" json:"status"` // pending:待支付 paid:已支付 cancelled:已取消 refunded:已退款 + PayTime *time.Time `json:"payTime"` + ExpireTime *time.Time `json:"expireTime"` + Remark string `gorm:"size:255" json:"remark"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// OrderItem 订单项 +type OrderItem struct { + ID uint `gorm:"primaryKey" json:"id"` + OrderID uint `gorm:"index" json:"orderId"` + ProductID uint `json:"productId"` + Name string `gorm:"size:100" json:"name"` + Price int `json:"price"` // 单价(分) + Quantity int `json:"quantity"` // 数量 +} diff --git a/server/internal/model/user.go b/server/internal/model/user.go new file mode 100644 index 0000000..ed580ab --- /dev/null +++ b/server/internal/model/user.go @@ -0,0 +1,34 @@ +package model + +import ( + "time" +) + +// User 用户模型 +type User struct { + ID uint `gorm:"primaryKey" json:"id"` + OpenID string `gorm:"uniqueIndex;size:64" json:"openId"` + UnionID string `gorm:"size:64" json:"unionId"` + Nickname string `gorm:"size:64" json:"nickname"` + Avatar string `gorm:"size:255" json:"avatar"` + Gender int `gorm:"default:0" json:"gender"` // 0:未知 1:男 2:女 + Phone string `gorm:"size:20" json:"phone"` + Email string `gorm:"size:100" json:"email"` + Status int `gorm:"default:1" json:"status"` // 1:正常 0:禁用 + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// UserProfile 用户资料扩展 +type UserProfile struct { + ID uint `gorm:"primaryKey" json:"id"` + UserID uint `gorm:"index" json:"userId"` + RealName string `gorm:"size:32" json:"realName"` + Birthday string `gorm:"size:10" json:"birthday"` // YYYY-MM-DD + BirthTime string `gorm:"size:8" json:"birthTime"` // HH:mm:ss + Gender int `gorm:"default:0" json:"gender"` + Zodiac string `gorm:"size:10" json:"zodiac"` // 生肖 + Constellation string `gorm:"size:20" json:"constellation"` // 星座 + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/server/internal/model/wish.go b/server/internal/model/wish.go new file mode 100644 index 0000000..ebf31e7 --- /dev/null +++ b/server/internal/model/wish.go @@ -0,0 +1,41 @@ +package model + +import ( + "time" +) + +// Wish 许愿模型 +type Wish struct { + ID uint `gorm:"primaryKey" json:"id"` + UserID uint `gorm:"index" json:"userId"` + TreeID uint `gorm:"index" json:"treeId"` // 许愿树ID + Content string `gorm:"size:500" json:"content"` // 许愿内容 + Type string `gorm:"size:20;default:'free'" json:"type"` // free:免费 paid:付费 + Position int `gorm:"default:0" json:"position"` // 位置(用于排序/覆盖) + Status int `gorm:"default:1" json:"status"` // 1:正常 0:隐藏/删除 + IsRobot bool `gorm:"default:false" json:"isRobot"` // 是否机器人发布 + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// WishTree 许愿树模型 +type WishTree struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:50" json:"name"` + Description string `gorm:"size:255" json:"description"` + MaxWishes int `gorm:"default:100" json:"maxWishes"` // 最大许愿条数 + Status int `gorm:"default:1" json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// WishProduct 许愿商品(付费许愿) +type WishProduct struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:50" json:"name"` // 商品名称 + Description string `gorm:"size:255" json:"description"` + Price int `json:"price"` // 价格(分) + Duration int `json:"duration"` // 展示时长(天) + Position int `json:"position"` // 优先位置 + Status int `gorm:"default:1" json:"status"` +} diff --git a/server/internal/service/user.go b/server/internal/service/user.go new file mode 100644 index 0000000..a8c8541 --- /dev/null +++ b/server/internal/service/user.go @@ -0,0 +1,26 @@ +package service + +import ( + "github.com/gouki/lunar-server/internal/model" +) + +// UserService 用户服务 +type UserService struct{} + +// GetUserByOpenID 根据OpenID获取用户 +func (s *UserService) GetUserByOpenID(openID string) (*model.User, error) { + // TODO: 实现数据库查询 + return &model.User{}, nil +} + +// CreateUser 创建用户 +func (s *UserService) CreateUser(user *model.User) error { + // TODO: 实现数据库创建 + return nil +} + +// UpdateUser 更新用户 +func (s *UserService) UpdateUser(user *model.User) error { + // TODO: 实现数据库更新 + return nil +} diff --git a/server/internal/service/wish.go b/server/internal/service/wish.go new file mode 100644 index 0000000..11cc337 --- /dev/null +++ b/server/internal/service/wish.go @@ -0,0 +1,38 @@ +package service + +import ( + "github.com/gouki/lunar-server/internal/model" +) + +// WishService 许愿服务 +type WishService struct{} + +// GetWishTree 获取许愿树 +func (s *WishService) GetWishTree(treeID uint) (*model.WishTree, error) { + // TODO: 实现数据库查询 + return &model.WishTree{}, nil +} + +// CreateWish 创建许愿 +func (s *WishService) CreateWish(wish *model.Wish) error { + // TODO: 实现数据库创建 + return nil +} + +// GetWishList 获取许愿列表 +func (s *WishService) GetWishList(treeID uint, page, pageSize int) ([]*model.Wish, int64, error) { + // TODO: 实现数据库查询 + return []*model.Wish{}, 0, nil +} + +// DeleteWish 删除许愿 +func (s *WishService) DeleteWish(id uint) error { + // TODO: 实现数据库删除 + return nil +} + +// CreateRobotWish 创建机器人许愿 +func (s *WishService) CreateRobotWish(treeID uint) error { + // TODO: 实现机器人自动许愿 + return nil +} diff --git a/server/migrations/001_init.sql b/server/migrations/001_init.sql new file mode 100644 index 0000000..b8089a7 --- /dev/null +++ b/server/migrations/001_init.sql @@ -0,0 +1,104 @@ +-- 用户表 +CREATE TABLE IF NOT EXISTS users ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + open_id VARCHAR(64) NOT NULL UNIQUE COMMENT '微信OpenID', + union_id VARCHAR(64) DEFAULT '' COMMENT '微信UnionID', + nickname VARCHAR(64) DEFAULT '' COMMENT '昵称', + avatar VARCHAR(255) DEFAULT '' COMMENT '头像', + gender TINYINT DEFAULT 0 COMMENT '性别 0:未知 1:男 2:女', + phone VARCHAR(20) DEFAULT '' COMMENT '手机号', + email VARCHAR(100) DEFAULT '' COMMENT '邮箱', + status TINYINT DEFAULT 1 COMMENT '状态 1:正常 0:禁用', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_open_id (open_id), + INDEX idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- 用户资料表 +CREATE TABLE IF NOT EXISTS user_profiles ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', + real_name VARCHAR(32) DEFAULT '' COMMENT '真实姓名', + birthday VARCHAR(10) DEFAULT '' COMMENT '生日 YYYY-MM-DD', + birth_time VARCHAR(8) DEFAULT '' COMMENT '出生时间 HH:mm:ss', + gender TINYINT DEFAULT 0 COMMENT '性别', + zodiac VARCHAR(10) DEFAULT '' COMMENT '生肖', + constellation VARCHAR(20) DEFAULT '' COMMENT '星座', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户资料表'; + +-- 订单表 +CREATE TABLE IF NOT EXISTS orders ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号', + user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', + type VARCHAR(20) NOT NULL COMMENT '订单类型 wish:许愿 vip:会员', + product_id BIGINT UNSIGNED DEFAULT 0 COMMENT '商品ID', + product_name VARCHAR(100) DEFAULT '' COMMENT '商品名称', + amount INT NOT NULL DEFAULT 0 COMMENT '金额(分)', + status VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending:待支付 paid:已支付 cancelled:已取消 refunded:已退款', + pay_time TIMESTAMP NULL COMMENT '支付时间', + expire_time TIMESTAMP NULL COMMENT '过期时间', + remark VARCHAR(255) DEFAULT '' COMMENT '备注', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_order_no (order_no), + INDEX idx_user_id (user_id), + INDEX idx_status (status), + INDEX idx_type (type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; + +-- 许愿树表 +CREATE TABLE IF NOT EXISTS wish_trees ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL COMMENT '名称', + description VARCHAR(255) DEFAULT '' COMMENT '描述', + max_wishes INT DEFAULT 100 COMMENT '最大许愿条数', + status TINYINT DEFAULT 1 COMMENT '状态', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树表'; + +-- 许愿表 +CREATE TABLE IF NOT EXISTS wishes ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', + tree_id BIGINT UNSIGNED NOT NULL COMMENT '许愿树ID', + content VARCHAR(500) NOT NULL COMMENT '许愿内容', + type VARCHAR(20) DEFAULT 'free' COMMENT '类型 free:免费 paid:付费', + position INT DEFAULT 0 COMMENT '位置', + status TINYINT DEFAULT 1 COMMENT '状态 1:正常 0:隐藏/删除', + is_robot TINYINT DEFAULT 0 COMMENT '是否机器人发布', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_tree_id (tree_id), + INDEX idx_status (status), + INDEX idx_type (type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿表'; + +-- 许愿商品表 +CREATE TABLE IF NOT EXISTS wish_products ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL COMMENT '商品名称', + description VARCHAR(255) DEFAULT '' COMMENT '描述', + price INT NOT NULL DEFAULT 0 COMMENT '价格(分)', + duration INT DEFAULT 7 COMMENT '展示时长(天)', + position INT DEFAULT 0 COMMENT '优先位置', + status TINYINT DEFAULT 1 COMMENT '状态', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿商品表'; + +-- 插入默认许愿树 +INSERT IGNORE INTO wish_trees (id, name, description, max_wishes) VALUES +(1, '祈福许愿树', '许下美好愿望,祈福平安顺遂', 100); + +-- 插入默认许愿商品 +INSERT IGNORE INTO wish_products (id, name, description, price, duration, position) VALUES +(1, '普通许愿条', '基础许愿条,展示7天', 100, 7, 0), +(2, '精品许愿条', '精品许愿条,展示30天,优先位置', 500, 30, 10), +(3, '至尊许愿条', '至尊许愿条,展示90天,置顶显示', 2000, 90, 100); diff --git a/server/scripts/build.sh b/server/scripts/build.sh new file mode 100644 index 0000000..5c5907d --- /dev/null +++ b/server/scripts/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# 生产环境构建脚本 + +echo "Building Lunar Server..." + +# 创建输出目录 +mkdir -p bin + +# 编译 Go 后端 +echo "Building Go backend..." +go build -o bin/lunar-server cmd/main.go + +# 编译前端(如果存在) +if [ -d "web" ]; then + echo "Building frontend..." + cd web + npm install + npm run build + cd .. +fi + +echo "Build complete!" +echo "Output: bin/lunar-server" diff --git a/server/scripts/dev.sh b/server/scripts/dev.sh new file mode 100644 index 0000000..47fad8d --- /dev/null +++ b/server/scripts/dev.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# 开发环境启动脚本 + +echo "Starting Lunar Server..." + +# 检查 .env.local 文件 +if [ ! -f "../.env.local" ]; then + echo "Warning: .env.local not found, using default configuration" +fi + +# 启动服务器 +go run cmd/main.go