feat: 添加许愿树功能和Go后端服务
- 新增许愿树页面(Canvas绘制、许愿条展示) - 新增许愿详情页面 - 新增许愿创建弹窗(免费/付费两种模式) - 新增网络请求封装 utils/request.js - 新增Go后端项目结构(Gin + Inertia.js) - 新增用户、订单、许愿数据模型 - 新增数据库迁移脚本 - 更新.gitignore补全忽略规则 - 更新.env.local配置(robot=2, 版本1.0.1) - 添加secrets目录说明文档
This commit is contained in:
+85
@@ -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/
|
||||||
+9
-1
@@ -7,7 +7,9 @@
|
|||||||
"pages/fortune/fortune",
|
"pages/fortune/fortune",
|
||||||
"pages/divination/divination",
|
"pages/divination/divination",
|
||||||
"pages/solar-terms/solar-terms",
|
"pages/solar-terms/solar-terms",
|
||||||
"pages/settings/settings"
|
"pages/settings/settings",
|
||||||
|
"pages/wish-tree/wish-tree",
|
||||||
|
"pages/wish-detail/wish-detail"
|
||||||
],
|
],
|
||||||
"window": {
|
"window": {
|
||||||
"backgroundTextStyle": "light",
|
"backgroundTextStyle": "light",
|
||||||
@@ -46,6 +48,12 @@
|
|||||||
"iconPath": "images/fortune.png",
|
"iconPath": "images/fortune.png",
|
||||||
"selectedIconPath": "images/fortune-active.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",
|
"pagePath": "pages/settings/settings",
|
||||||
"text": "设置",
|
"text": "设置",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 67 B |
Binary file not shown.
|
After Width: | Height: | Size: 67 B |
@@ -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 : ''}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "许愿详情",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<view class="container">
|
||||||
|
<view class="loading" wx:if="{{loading}}">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="wish-detail" wx:else>
|
||||||
|
<view class="wish-card {{wish.type}}">
|
||||||
|
<view class="wish-header">
|
||||||
|
<image class="avatar" src="{{wish.user.avatar || '/images/default-avatar.png'}}" mode="aspectFill"></image>
|
||||||
|
<view class="user-info">
|
||||||
|
<text class="nickname">{{wish.user.nickname}}</text>
|
||||||
|
<text class="time">{{wish.createdAt}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="wish-badge" wx:if="{{wish.type === 'paid'}}">
|
||||||
|
<text>VIP</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="wish-content">
|
||||||
|
<text>{{wish.content}}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="wish-footer">
|
||||||
|
<view class="wish-type">
|
||||||
|
<text class="type-tag {{wish.type}}">{{wish.type === 'paid' ? '付费许愿' : '免费许愿'}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="wish-actions">
|
||||||
|
<button class="btn-action" bindtap="onShare">
|
||||||
|
<text class="icon">📤</text>
|
||||||
|
<text>分享</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 相关推荐 -->
|
||||||
|
<view class="recommend-section">
|
||||||
|
<view class="section-title">更多愿望</view>
|
||||||
|
<view class="recommend-list">
|
||||||
|
<view class="recommend-item" wx:for="{{[1,2,3]}}" wx:key="*this">
|
||||||
|
<text class="recommend-text">愿所有美好如期而至</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "许愿树",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"backgroundTextStyle": "dark"
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<view class="container">
|
||||||
|
<!-- 许愿树画布 -->
|
||||||
|
<view class="canvas-container">
|
||||||
|
<canvas
|
||||||
|
canvas-id="wishTree"
|
||||||
|
class="wish-tree-canvas"
|
||||||
|
style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"
|
||||||
|
></canvas>
|
||||||
|
|
||||||
|
<!-- 许愿条覆盖层 -->
|
||||||
|
<view class="wishes-overlay">
|
||||||
|
<view
|
||||||
|
wx:for="{{wishes}}"
|
||||||
|
wx:key="id"
|
||||||
|
class="wish-tag {{item.type}} {{item.isRobot ? 'robot' : ''}}"
|
||||||
|
style="left: {{item.x}}px; top: {{item.y}}px;"
|
||||||
|
bindtap="viewWishDetail"
|
||||||
|
data-id="{{item.id}}"
|
||||||
|
>
|
||||||
|
<text class="wish-text">{{item.content}}</text>
|
||||||
|
<view class="wish-badge" wx:if="{{item.type === 'paid'}}">VIP</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 许愿按钮 -->
|
||||||
|
<view class="action-bar">
|
||||||
|
<button class="btn-primary btn-wish" bindtap="openCreateModal">
|
||||||
|
<text class="icon">🙏</text>
|
||||||
|
<text>我要许愿</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 许愿说明 -->
|
||||||
|
<view class="tips-section">
|
||||||
|
<view class="tip-item">
|
||||||
|
<text class="tip-icon">🎋</text>
|
||||||
|
<text class="tip-text">免费许愿:20字以内,可能被覆盖</text>
|
||||||
|
</view>
|
||||||
|
<view class="tip-item">
|
||||||
|
<text class="tip-icon">✨</text>
|
||||||
|
<text class="tip-text">付费许愿:100字以内,优先展示,长期保留</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 许愿弹窗 -->
|
||||||
|
<view class="modal" wx:if="{{showCreateModal}}">
|
||||||
|
<view class="modal-mask" bindtap="closeCreateModal"></view>
|
||||||
|
<view class="modal-content">
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">许下心愿</text>
|
||||||
|
<text class="modal-close" bindtap="closeCreateModal">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="modal-body">
|
||||||
|
<!-- 许愿类型选择 -->
|
||||||
|
<view class="type-selector">
|
||||||
|
<view
|
||||||
|
class="type-item {{wishType === 'free' ? 'active' : ''}}"
|
||||||
|
bindtap="selectType"
|
||||||
|
data-type="free"
|
||||||
|
>
|
||||||
|
<text class="type-name">免费许愿</text>
|
||||||
|
<text class="type-desc">20字以内</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="type-item {{wishType === 'paid' ? 'active' : ''}}"
|
||||||
|
bindtap="selectType"
|
||||||
|
data-type="paid"
|
||||||
|
>
|
||||||
|
<text class="type-name">付费许愿</text>
|
||||||
|
<text class="type-desc">更多特权</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 许愿内容输入 -->
|
||||||
|
<textarea
|
||||||
|
class="wish-input"
|
||||||
|
placeholder="请输入你的愿望..."
|
||||||
|
maxlength="{{wishType === 'free' ? maxFreeLength : maxPaidLength}}"
|
||||||
|
bindinput="onInputContent"
|
||||||
|
value="{{wishContent}}"
|
||||||
|
></textarea>
|
||||||
|
<view class="input-count">
|
||||||
|
{{wishContent.length}}/{{wishType === 'free' ? maxFreeLength : maxPaidLength}}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 付费商品选择 -->
|
||||||
|
<view class="product-section" wx:if="{{wishType === 'paid'}}">
|
||||||
|
<view class="section-title">选择许愿商品</view>
|
||||||
|
<view class="product-list">
|
||||||
|
<view
|
||||||
|
wx:for="{{products}}"
|
||||||
|
wx:key="id"
|
||||||
|
class="product-item {{selectedProduct && selectedProduct.id === item.id ? 'active' : ''}}"
|
||||||
|
bindtap="selectProduct"
|
||||||
|
data-id="{{item.id}}"
|
||||||
|
>
|
||||||
|
<view class="product-info">
|
||||||
|
<text class="product-name">{{item.name}}</text>
|
||||||
|
<text class="product-desc">展示{{item.duration}}天</text>
|
||||||
|
</view>
|
||||||
|
<view class="product-price">¥{{item.price / 100}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="modal-footer">
|
||||||
|
<button class="btn-outline" bindtap="closeCreateModal">取消</button>
|
||||||
|
<button class="btn-primary" bindtap="submitWish">
|
||||||
|
{{wishType === 'paid' ? '支付并许愿' : '提交许愿'}}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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` 初始化数据库。
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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": "系统设置",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"` // 数量
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user