Browse Source

feat: 营养师/管家小程序入驻申请流程

- 后端: NutritionistController 添加 /api/nutritionist/apply 提交接口
- 小程序: 新增 pages/nutritionist/apply 营养师入驻页面
- 小程序: 新增 pages/butler/apply 管家入驻页面
- pages.json: 注册 nutritionist/butler 两个子包
- api.js: 新增 nutritionistApply/nutritionistStatus/butlerApply 函数
Xiaogang Liao 3 months ago
parent
commit
1d6520e475

+ 54 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/NutritionistController.java

@@ -0,0 +1,54 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.UserService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.Map;
+
+@RestController("nutritionistController")
+@RequestMapping("/api/nutritionist")
+public class NutritionistController {
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private UserService userService;
+
+    @PostMapping("/apply")
+    public Result<String> apply(@RequestBody Map<String, Object> params,
+                                 @RequestAttribute("userId") Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        if (user.getNutritionistStatus() != null && !"rejected".equals(user.getNutritionistStatus())) {
+            return Result.error("已有营养师申请,请勿重复提交");
+        }
+        user.setNutritionistStatus("pending");
+        user.setNutritionistRejectReason(null);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+
+        userService.addRole(user.getId(), "nutritionist");
+
+        return Result.success("营养师申请已提交,请等待审核");
+    }
+
+    @PostMapping("/status")
+    public Result<Map<String, Object>> getStatus(@RequestAttribute("userId") Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        java.util.Map<String, Object> data = new java.util.HashMap<>();
+        data.put("nutritionistStatus", user.getNutritionistStatus());
+        data.put("nutritionistRejectReason", user.getNutritionistRejectReason());
+        return Result.success(data);
+    }
+}

+ 22 - 0
cfc-frontend/pages.json

@@ -721,6 +721,28 @@
         }
       ]
     },
+    {
+      "root": "pages/nutritionist",
+      "pages": [
+        {
+          "path": "apply",
+          "style": {
+            "navigationBarTitleText": "营养师入驻"
+          }
+        }
+      ]
+    },
+    {
+      "root": "pages/butler",
+      "pages": [
+        {
+          "path": "apply",
+          "style": {
+            "navigationBarTitleText": "管家入驻"
+          }
+        }
+      ]
+    },
     {
       "root": "pages/stats",
       "pages": [

+ 200 - 0
cfc-frontend/pages/butler/apply/index.vue

@@ -0,0 +1,200 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">成为管家</text>
+      <text class="subtitle">提交您的管家资质,为更多家庭提供专业服务</text>
+    </view>
+
+    <view class="form">
+      <view class="form-item">
+        <text class="label">真实姓名</text>
+        <input class="input" v-model="formData.realName" placeholder="请输入真实姓名" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">手机号</text>
+        <input class="input" v-model="formData.phone" type="number" placeholder="请输入手机号" maxlength="11" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">管家等级</text>
+        <picker mode="selector" :range="tiers" range-key="label" @change="onTierChange">
+          <view class="picker">
+            <text v-if="formData.tier">{{ getTierLabel(formData.tier) }}</text>
+            <text v-else class="placeholder">请选择管家等级</text>
+            <text class="arrow">›</text>
+          </view>
+        </picker>
+      </view>
+
+      <view class="form-item">
+        <text class="label">资质说明</text>
+        <textarea class="textarea" v-model="formData.description" placeholder="请说明您的管家资质、从业经验等" maxlength="500" />
+        <text class="counter">{{ formData.description.length }}/500</text>
+      </view>
+
+      <button class="btn-submit" :disabled="submitting" @click="handleSubmit">
+        {{ submitting ? '提交中...' : '提交申请' }}
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { butlerApply } from '../../utils/api'
+
+export default {
+  data() {
+    return {
+      tiers: [
+        { value: 'bronze', label: '青铜管家' },
+        { value: 'silver', label: '白银管家' },
+        { value: 'gold', label: '黄金管家' },
+        { value: 'platinum', label: '铂金管家' }
+      ],
+      formData: {
+        realName: '',
+        phone: '',
+        tier: '',
+        description: ''
+      },
+      submitting: false
+    }
+  },
+  methods: {
+    onTierChange(e) {
+      this.formData.tier = this.tiers[e.detail.value].value
+    },
+    getTierLabel(value) {
+      const item = this.tiers.find(t => t.value === value)
+      return item ? item.label : value
+    },
+    async handleSubmit() {
+      if (!this.formData.realName) {
+        uni.showToast({ title: '请输入真实姓名', icon: 'none' })
+        return
+      }
+      if (!this.formData.phone) {
+        uni.showToast({ title: '请输入手机号', icon: 'none' })
+        return
+      }
+      this.submitting = true
+      try {
+        const res = await butlerApply({
+          tier: this.formData.tier,
+          description: this.formData.description
+        })
+        if (res.code === 200) {
+          uni.showModal({
+            title: '申请成功',
+            content: '您的管家申请已提交,请等待审核',
+            showCancel: false,
+            success: () => uni.navigateBack()
+          })
+        } else {
+          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '提交失败', icon: 'none' })
+      } finally {
+        this.submitting = false
+      }
+    }
+  }
+}
+</script>
+
+<style>
+.container {
+  padding: 30rpx;
+  background: #f8f8f8;
+  min-height: 100vh;
+}
+.header {
+  text-align: center;
+  padding: 40rpx 0;
+}
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+}
+.subtitle {
+  font-size: 28rpx;
+  color: #666;
+  margin-top: 16rpx;
+  display: block;
+}
+.form {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
+}
+.form-item {
+  margin-bottom: 40rpx;
+}
+.label {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+  font-weight: bold;
+}
+.input {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #e0e0e0;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+.picker {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #e0e0e0;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  box-sizing: border-box;
+}
+.placeholder {
+  color: #999;
+}
+.arrow {
+  font-size: 32rpx;
+  color: #999;
+}
+.textarea {
+  width: 100%;
+  height: 200rpx;
+  border: 1rpx solid #e0e0e0;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+.counter {
+  font-size: 24rpx;
+  color: #999;
+  text-align: right;
+  display: block;
+  margin-top: 10rpx;
+}
+.btn-submit {
+  width: 100%;
+  height: 90rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 45rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-top: 40rpx;
+}
+.btn-submit[disabled] {
+  opacity: 0.6;
+}
+</style>

+ 153 - 0
cfc-frontend/pages/nutritionist/apply/index.vue

@@ -0,0 +1,153 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">成为营养师</text>
+      <text class="subtitle">提交您的专业资质,成为认证家庭营养师</text>
+    </view>
+
+    <view class="form">
+      <view class="form-item">
+        <text class="label">真实姓名</text>
+        <input class="input" v-model="formData.realName" placeholder="请输入真实姓名" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">手机号</text>
+        <input class="input" v-model="formData.phone" type="number" placeholder="请输入手机号" maxlength="11" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">专业资质说明</text>
+        <textarea class="textarea" v-model="formData.qualification" placeholder="请说明您的营养师资质、从业经验等" maxlength="500" />
+        <text class="counter">{{ formData.qualification.length }}/500</text>
+      </view>
+
+      <button class="btn-submit" :disabled="submitting" @click="handleSubmit">
+        {{ submitting ? '提交中...' : '提交申请' }}
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { nutritionistApply } from '../../../utils/api'
+
+export default {
+  data() {
+    return {
+      formData: {
+        realName: '',
+        phone: '',
+        qualification: ''
+      },
+      submitting: false
+    }
+  },
+  methods: {
+    async handleSubmit() {
+      if (!this.formData.realName) {
+        uni.showToast({ title: '请输入真实姓名', icon: 'none' })
+        return
+      }
+      if (!this.formData.phone) {
+        uni.showToast({ title: '请输入手机号', icon: 'none' })
+        return
+      }
+      this.submitting = true
+      try {
+        const res = await nutritionistApply(this.formData)
+        if (res.code === 200) {
+          uni.showModal({
+            title: '申请成功',
+            content: '您的营养师申请已提交,请等待审核',
+            showCancel: false,
+            success: () => uni.navigateBack()
+          })
+        } else {
+          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '提交失败', icon: 'none' })
+      } finally {
+        this.submitting = false
+      }
+    }
+  }
+}
+</script>
+
+<style>
+.container {
+  padding: 30rpx;
+  background: #f8f8f8;
+  min-height: 100vh;
+}
+.header {
+  text-align: center;
+  padding: 40rpx 0;
+}
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+}
+.subtitle {
+  font-size: 28rpx;
+  color: #666;
+  margin-top: 16rpx;
+  display: block;
+}
+.form {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
+}
+.form-item {
+  margin-bottom: 40rpx;
+}
+.label {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+  font-weight: bold;
+}
+.input {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #e0e0e0;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+.textarea {
+  width: 100%;
+  height: 200rpx;
+  border: 1rpx solid #e0e0e0;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+.counter {
+  font-size: 24rpx;
+  color: #999;
+  text-align: right;
+  display: block;
+  margin-top: 10rpx;
+}
+.btn-submit {
+  width: 100%;
+  height: 90rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 45rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-top: 40rpx;
+}
+.btn-submit[disabled] {
+  opacity: 0.6;
+}
+</style>