Prechádzať zdrojové kódy

feat: 集成后端登录接口,增加图形验证码

- api/auth.ts: 对接真实登录API (/dgapi/mobile/user/login) 和验证码API
- LoginView.vue: 新增图形验证码展示与输入,点击刷新验证码
- request.ts: 非200响应抛出异常,便于调用方统一错误处理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tanlie 3 týždňov pred
rodič
commit
71bb9ce64e
23 zmenil súbory, kde vykonal 365 pridanie a 32 odobranie
  1. BIN
      wishing-admin-vue3/admin/logo.png
  2. 1 0
      wishing-admin-vue3/src/views/login/LoginForm.vue
  3. 1 2
      wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/BaseEntity.java
  4. 27 1
      wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/WishingUser.java
  5. BIN
      wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/BaseEntity.class
  6. BIN
      wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/WishingTree.class
  7. BIN
      wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/WishingUser.class
  8. BIN
      wishing-platform/platform-entity/platform-entity-wishing/target/platform-entity-wishing-1.0.0-SNAPSHOT.jar
  9. 2 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/MyBatisPlusConfig.java
  10. 54 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/UserController.java
  11. 20 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/mapper/WishingUserMapper.java
  12. 39 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/LoginReq.java
  13. 19 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/CaptchaResp.java
  14. 23 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/LoginResp.java
  15. 20 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/LoginService.java
  16. 79 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/LoginServiceImpl.java
  17. 2 1
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/WishServiceImpl.java
  18. 2 3
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/WishTreeServiceImpl.java
  19. 22 12
      wishing-tree-h5/src/api/auth.ts
  20. 3 1
      wishing-tree-h5/src/api/request.ts
  21. 5 0
      wishing-tree-h5/src/views/HomeView.vue
  22. 46 12
      wishing-tree-h5/src/views/LoginView.vue
  23. BIN
      wishing-tree-h5/wish.zip

BIN
wishing-admin-vue3/admin/logo.png


+ 1 - 0
wishing-admin-vue3/src/views/login/LoginForm.vue

@@ -112,6 +112,7 @@ const rules: FormRules = {
 async function doFetchDigitCaptcha() {
   try {
     const res = await fetchDigitCaptcha()
+    console.log(res.data)
     const key = Object.keys(res.data)[0]
     form.captchaKey = key
     imageCode.value = res.data[key]

+ 1 - 2
wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/BaseEntity.java

@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.Version;
 import lombok.Data;
 
-import java.io.Serializable;
 import java.time.LocalDateTime;
 
 /**
@@ -14,7 +13,7 @@ import java.time.LocalDateTime;
  * @author lie tan
  */
 @Data
-public class BaseEntity implements Serializable {
+public class BaseEntity {
 
     @Version
     private Integer version;

+ 27 - 1
wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/WishingUser.java

@@ -1,16 +1,42 @@
 package cn.qinys.platform.entity.wishing;
 
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.Locale;
 
 /**
  * @author lie tan
  * @description
  * @date 2026-05-17 21:33
  **/
+@EqualsAndHashCode(callSuper = true)
 @Data
 @TableName("wishing_user")
-public class WishingUser implements Serializable {
+public class WishingUser extends BaseEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String nickname;
+
+    private String mobile;
+
+    private String avatar;
+
+    private String password;
+
+    private String salt;
+
+    private Integer status;
+
+    private LocalDateTime lastLoginAt;
+
+    private Integer loginCount;
+
 }

BIN
wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/BaseEntity.class


BIN
wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/WishingTree.class


BIN
wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/WishingUser.class


BIN
wishing-platform/platform-entity/platform-entity-wishing/target/platform-entity-wishing-1.0.0-SNAPSHOT.jar


+ 2 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/MyBatisPlusConfig.java

@@ -3,6 +3,7 @@ package cn.qinys.platform.config;
 
 import com.baomidou.mybatisplus.annotation.DbType;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -25,6 +26,7 @@ public class MyBatisPlusConfig {
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
         interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
         // 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
         return interceptor;

+ 54 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/UserController.java

@@ -0,0 +1,54 @@
+package cn.qinys.platform.mobile.controller;
+
+import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.mobile.req.LoginReq;
+import cn.qinys.platform.mobile.resp.CaptchaResp;
+import cn.qinys.platform.mobile.resp.LoginResp;
+import cn.qinys.platform.mobile.service.LoginService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-06 13:14
+ **/
+
+@Slf4j
+@RestController
+@RequestMapping("/dgapi/mobile/user")
+public class UserController {
+
+    @Resource
+    LoginService loginService;
+
+
+    /**
+     * 获取验证码
+     *
+     * @return
+     */
+    @GetMapping("/login/captcha")
+    public Result<CaptchaResp> getCaptcha(@RequestParam(required = false, defaultValue = "1") String type) {
+        CaptchaResp captcha = loginService.getCaptcha(type);
+        return new Result<>(captcha);
+    }
+
+
+    /**
+     * 登录
+     *
+     * @param req
+     * @return
+     */
+    // @SensitiveRequest
+    @PostMapping("/login")
+    public Result<LoginResp> login(@RequestBody @Valid LoginReq req) {
+        LoginResp res = loginService.login(req);
+        return new Result<>(res);
+    }
+
+
+}

+ 20 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/mapper/WishingUserMapper.java

@@ -0,0 +1,20 @@
+package cn.qinys.platform.mobile.mapper;
+
+import cn.qinys.platform.entity.wishing.WishingUser;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-05-23 12:41
+ **/
+@Mapper
+public interface WishingUserMapper extends BaseMapper<WishingUser> {
+
+    @Select("SELECT * FROM wishing_user WHERE mobile = #{mobile} AND status = 1 AND is_deleted = 0 LIMIT 1")
+    WishingUser selectByMobile(@Param("mobile") String mobile);
+
+}

+ 39 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/LoginReq.java

@@ -0,0 +1,39 @@
+package cn.qinys.platform.mobile.req;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-06 13:21
+ **/
+@Data
+public class LoginReq implements Serializable {
+    /**
+     * 手机号 加密传输
+     */
+    @NotBlank(message = "电话号码不能为空")
+    private String mobile;
+
+    /**
+     * 验证码 加密传输
+     */
+    @NotBlank(message = "验证码不能为空")
+    private String code;
+
+    /**
+     * 验证码
+     */
+    @NotNull(message = "验证码不能为空")
+    private String captcha;
+
+    /**
+     * 验证码key
+     */
+    @NotNull(message = "验证码key不能为空")
+    private String captchaKey;
+}

+ 19 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/CaptchaResp.java

@@ -0,0 +1,19 @@
+package cn.qinys.platform.mobile.resp;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-06 13:48
+ **/
+@Data
+public class CaptchaResp implements Serializable {
+
+    private String key;
+
+    private String value;
+
+}

+ 23 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/LoginResp.java

@@ -0,0 +1,23 @@
+package cn.qinys.platform.mobile.resp;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-06 13:22
+ **/
+@Data
+public class LoginResp implements Serializable {
+
+    private String token;
+
+    private String mobile;
+
+    private String nickname;
+    
+    private String avatar;
+
+}

+ 20 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/LoginService.java

@@ -0,0 +1,20 @@
+package cn.qinys.platform.mobile.service;
+
+import cn.qinys.platform.mobile.req.LoginReq;
+import cn.qinys.platform.mobile.resp.CaptchaResp;
+import cn.qinys.platform.mobile.resp.LoginResp;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-06 13:28
+ **/
+
+public interface LoginService {
+
+
+    LoginResp   login(LoginReq query);
+
+    CaptchaResp getCaptcha(String type);
+
+}

+ 79 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/LoginServiceImpl.java

@@ -0,0 +1,79 @@
+package cn.qinys.platform.mobile.service.impl;
+
+import cn.qinys.platform.base.exceptions.BizException;
+import cn.qinys.platform.base.service.AbstractLoginService;
+import cn.qinys.platform.base.service.ICaptchaService;
+import cn.qinys.platform.base.vo.LoginUserInfo;
+import cn.qinys.platform.entity.wishing.WishingUser;
+import cn.qinys.platform.mobile.mapper.WishingUserMapper;
+import cn.qinys.platform.mobile.req.LoginReq;
+import cn.qinys.platform.mobile.resp.CaptchaResp;
+import cn.qinys.platform.mobile.resp.LoginResp;
+import cn.qinys.platform.mobile.service.LoginService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-06 13:35
+ **/
+@Service
+public class LoginServiceImpl extends AbstractLoginService implements LoginService {
+
+
+    @Resource
+    private ICaptchaService captchaService;
+    @Resource
+    private WishingUserMapper userMapper;
+
+
+    @Override
+    public LoginResp login(LoginReq query) {
+        WishingUser user = userMapper.selectByMobile(query.getMobile());
+        if (user == null) {
+            throw new BizException("用户不存在");
+        }
+        user.setLastLoginAt(LocalDateTime.now());
+        user.setLoginCount(user.getLoginCount() + 1);
+        userMapper.updateById(user);
+
+        String token = UUID.randomUUID().toString().replaceAll("-", "");
+        String wxId = "wx_" + user.getMobile();
+        LoginUserInfo userInfo = new LoginUserInfo();
+        userInfo.setId(String.valueOf(user.getId()));
+        userInfo.setMobile(user.getMobile());
+        userInfo.setUsername(user.getNickname());
+        userInfo.setWxId(wxId);
+        this.setLoginUserInfo(userInfo, token);
+        this.kicKOut(wxId);  // 踢人下线
+        this.recordToken(wxId, token);
+        // 返回前端
+        LoginResp resp = new LoginResp();
+        resp.setToken(token);
+        resp.setMobile(user.getMobile());
+        resp.setNickname(user.getNickname());
+        resp.setAvatar(user.getAvatar());
+        return resp;
+    }
+
+    @Override
+    public CaptchaResp getCaptcha(String type) {
+        Map<String, String> render = captchaService.render(type);
+        CaptchaResp resp = new CaptchaResp();
+        AtomicReference<String> key = new AtomicReference<>();
+        AtomicReference<String> value = new AtomicReference<>();
+        render.forEach((k, v) -> {
+            key.set(k);
+            value.set(v);
+        });
+        resp.setKey(key.get());
+        resp.setValue(value.get());
+        return resp;
+    }
+}

+ 2 - 1
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/WishServiceImpl.java → wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/WishServiceImpl.java

@@ -1,4 +1,4 @@
-package cn.qinys.platform.mobile.service;
+package cn.qinys.platform.mobile.service.impl;
 
 import cn.qinys.platform.entity.wishing.Wish;
 import cn.qinys.platform.entity.wishing.WishingTree;
@@ -7,6 +7,7 @@ import cn.qinys.platform.mobile.mapper.WishingTreeMapper;
 import cn.qinys.platform.mobile.req.WishCreateReq;
 import cn.qinys.platform.mobile.resp.WishDetailResp;
 import cn.qinys.platform.mobile.resp.WishListResp;
+import cn.qinys.platform.mobile.service.WishService;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.fasterxml.jackson.core.JsonProcessingException;

+ 2 - 3
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/WishTreeServiceImpl.java → wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/WishTreeServiceImpl.java

@@ -1,16 +1,15 @@
-package cn.qinys.platform.mobile.service;
+package cn.qinys.platform.mobile.service.impl;
 
 import cn.qinys.platform.entity.wishing.WishingTree;
 import cn.qinys.platform.mobile.mapper.WishingTreeMapper;
 import cn.qinys.platform.mobile.req.WishingTreeListReq;
 import cn.qinys.platform.mobile.resp.WishingTreeListResp;
+import cn.qinys.platform.mobile.service.WishTreeService;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
-import java.math.BigDecimal;
-import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;

+ 22 - 12
wishing-tree-h5/src/api/auth.ts

@@ -1,18 +1,28 @@
-// Mock login
+import { request } from './request'
+
+export async function getCaptcha() {
+  const res = await request<{ key: string; value: string }>('/mobile/user/login/captcha')
+  return res.data
+}
+
 export async function sendSms(phone: string) {
+  // 后端暂无发送短信接口,仅做前端倒计时模拟
   console.log(`[Mock] Sending SMS to ${phone}`)
-  return Promise.resolve({ code: 0, data: null })
+  return Promise.resolve()
 }
 
-export async function login(phone: string, code: string) {
-  console.log(`[Mock] Login with ${phone}, code: ${code}`)
-  return Promise.resolve({
-    code: 0,
-    data: {
-      token: 'mock_token_' + Date.now(),
-      nickname: '用户' + phone.slice(-4),
-      phone,
-      avatarUrl: '',
+export async function login(phone: string, code: string, captcha: string, captchaKey: string) {
+  const res = await request<{ token: string; mobile: string; nickname: string; avatar: string }>(
+    '/mobile/user/login',
+    {
+      method: 'POST',
+      body: JSON.stringify({ mobile: phone, code, captcha, captchaKey }),
     },
-  })
+  )
+  return {
+    token: res.data.token,
+    phone: res.data.mobile,
+    nickname: res.data.nickname,
+    avatarUrl: res.data.avatar || '',
+  }
 }

+ 3 - 1
wishing-tree-h5/src/api/request.ts

@@ -29,7 +29,9 @@ export async function request<T = any>(url: string, options?: RequestInit): Prom
     })
     const json = await res.json()
     if (json.code !== 200) {
-      showToast(json.msg || '请求失败')
+      const errMsg = json.msg || '请求失败'
+      showToast(errMsg)
+      throw new Error(errMsg)
     }
     return json
   } catch (err: any) {

+ 5 - 0
wishing-tree-h5/src/views/HomeView.vue

@@ -69,6 +69,8 @@ async function loadTrees() {
   }
 }
 
+
+
 async function onRefresh() {
   refreshing.value = true
   await locationStore.refreshLocation()
@@ -79,6 +81,9 @@ async function onRefresh() {
 function goTree(id: number) {
   router.push(`/tree/${id}`)
 }
+/*const goTrees = (id: number) => {
+  return router.push(`/tree/${id}`)
+}*/
 
 onMounted(async () => {
   locationStore.startWatch(5000)

+ 46 - 12
wishing-tree-h5/src/views/LoginView.vue

@@ -39,6 +39,23 @@
               </van-button>
             </template>
           </van-field>
+          <van-field
+            v-model="captchaText"
+            name="captcha"
+            label="图形验证"
+            placeholder="请输入图片中的字符"
+            maxlength="4"
+            :rules="[{ required: true, message: '请输入图形验证码' }]"
+          >
+            <template #button>
+              <img
+                v-if="captchaImg"
+                :src="captchaImg"
+                class="captcha-img"
+                @click="refreshCaptcha"
+              />
+            </template>
+          </van-field>
         </van-cell-group>
 
         <div class="submit-wrap">
@@ -47,34 +64,45 @@
           </van-button>
         </div>
       </van-form>
-
-      <p class="login-tip">Mock 模式:输入任意手机号和验证码即可登录</p>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { ref, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { showSuccessToast, showToast } from 'vant'
 import { useUserStore } from '@/stores/user'
-import { sendSms, login } from '@/api/auth'
+import { sendSms, login as loginApi, getCaptcha as getCaptchaApi } from '@/api/auth'
 
 const router = useRouter()
 const userStore = useUserStore()
 
 const phone = ref('')
 const code = ref('')
+const captchaText = ref('')
+const captchaImg = ref('')
+const captchaKey = ref('')
 const countdown = ref(0)
 const logging = ref(false)
 
+async function refreshCaptcha() {
+  try {
+    const captcha = await getCaptchaApi()
+    captchaKey.value = captcha.key
+    captchaImg.value = 'data:image/png;base64,' + captcha.value
+  } catch {
+    // ignore
+  }
+}
+
 function sendCode() {
   if (!/^1[3-9]\d{9}$/.test(phone.value)) {
     showToast('请输入正确的手机号')
     return
   }
   sendSms(phone.value)
-  showToast('验证码已发送(Mock模式任意6位数字)')
+  showToast('验证码已发送')
   countdown.value = 60
   const t = setInterval(() => {
     countdown.value--
@@ -85,14 +113,21 @@ function sendCode() {
 async function handleLogin() {
   logging.value = true
   try {
-    const res = await login(phone.value, code.value)
-    userStore.login(res.data)
+    const userData = await loginApi(phone.value, code.value, captchaText.value, captchaKey.value)
+    userStore.login(userData)
     showSuccessToast('登录成功')
     router.replace('/')
+  } catch (err: any) {
+    showToast(err?.message || '登录失败')
+    refreshCaptcha()
   } finally {
     logging.value = false
   }
 }
+
+onMounted(() => {
+  refreshCaptcha()
+})
 </script>
 
 <style scoped>
@@ -118,10 +153,9 @@ async function handleLogin() {
 .submit-wrap {
   padding: 24px 16px;
 }
-.login-tip {
-  text-align: center;
-  font-size: 12px;
-  color: #bbb;
-  margin-top: 20px;
+.captcha-img {
+  height: 36px;
+  cursor: pointer;
+  border-radius: 4px;
 }
 </style>

BIN
wishing-tree-h5/wish.zip