diff --git a/pay-mall-api/src/main/java/edu/whut/api/IAuthService.java b/pay-mall-api/src/main/java/edu/whut/api/IAuthService.java new file mode 100644 index 0000000..f9efb27 --- /dev/null +++ b/pay-mall-api/src/main/java/edu/whut/api/IAuthService.java @@ -0,0 +1,11 @@ +package edu.whut.api; + +import edu.whut.api.response.Response; + +public interface IAuthService { + + Response weixinQrCodeTicket(); + + Response checkLogin(String ticket); + +} diff --git a/pay-mall-app/src/main/java/edu/whut/config/GuavaConfig.java b/pay-mall-app/src/main/java/edu/whut/config/GuavaConfig.java index a411e36..ae8f5bf 100644 --- a/pay-mall-app/src/main/java/edu/whut/config/GuavaConfig.java +++ b/pay-mall-app/src/main/java/edu/whut/config/GuavaConfig.java @@ -10,10 +10,19 @@ import java.util.concurrent.TimeUnit; @Configuration public class GuavaConfig { - @Bean(name = "cache") - public Cache cache() { + @Bean(name = "weixinAccessToken") // 注册名为 weixinAccessToken 的缓存 Bean + public Cache weixinAccessToken() { + // 创建一个 Guava 本地缓存:写入后 2 小时过期 return CacheBuilder.newBuilder() - .expireAfterWrite(3, TimeUnit.SECONDS) + .expireAfterWrite(2, TimeUnit.HOURS) // 设置缓存条目在写入后 2 小时自动失效 + .build(); + } + + @Bean(name = "openidToken") // 注册名为 openidToken 的缓存 Bean + public Cache openidToken() { + // 创建一个 Guava 本地缓存:写入后 1 小时过期 + return CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) // 设置缓存条目在写入后 1 小时自动失效 .build(); } diff --git a/pay-mall-app/src/main/java/edu/whut/config/Retrofit2Config.java b/pay-mall-app/src/main/java/edu/whut/config/Retrofit2Config.java new file mode 100644 index 0000000..7fe223c --- /dev/null +++ b/pay-mall-app/src/main/java/edu/whut/config/Retrofit2Config.java @@ -0,0 +1,38 @@ +package edu.whut.config; + +import edu.whut.infrastructure.gateway.IWeixinApiService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; + +@Slf4j +@Configuration +public class Retrofit2Config { + + // 微信开放平台的基础 URL,后续所有接口都会在这个前缀下拼接路径 + private static final String BASE_URL = "https://api.weixin.qq.com/"; + + /** + * 创建一个 Retrofit 对象,并注册为 Spring Bean + * - baseUrl:设置所有请求的公共前缀 + * - addConverterFactory:添加 Jackson 转换器,用于 JSON <-> Java 对象的自动映射 + */ + @Bean + public Retrofit retrofit() { + return new Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(JacksonConverterFactory.create()).build(); + } + + /** + * 通过 Retrofit 动态生成 IWeixinApiService 接口的实现, + * 并注册为 Spring 容器中的 Bean,方便业务层直接注入使用 + */ + @Bean + public IWeixinApiService weixinApiService(Retrofit retrofit) { + return retrofit.create(IWeixinApiService.class); + } + +} diff --git a/pay-mall-app/src/main/resources/application-dev.yml b/pay-mall-app/src/main/resources/application-dev.yml index 9eebd99..08e4035 100644 --- a/pay-mall-app/src/main/resources/application-dev.yml +++ b/pay-mall-app/src/main/resources/application-dev.yml @@ -39,7 +39,10 @@ spring: weixin: config: originalid: gh_b748269e1f4c - token: b8b6 + token: asdf + app-id: wx7cc74be9b340b26e + app-secret: d4e73551512c6dc7a2e8f746c26b7f2c + template_id: ARDqdKXuGvASjsDqXzeunq0P8chMQ7tXk_4-BPULJ6U # 日志 logging: diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/auth/adapter/port/ILoginPort.java b/pay-mall-domain/src/main/java/edu/whut/domain/auth/adapter/port/ILoginPort.java new file mode 100644 index 0000000..007b62e --- /dev/null +++ b/pay-mall-domain/src/main/java/edu/whut/domain/auth/adapter/port/ILoginPort.java @@ -0,0 +1,11 @@ +package edu.whut.domain.auth.adapter.port; + +import java.io.IOException; + +public interface ILoginPort { + + String createQrCodeTicket() throws IOException; + + void sendLoginTemplate(String openid) throws IOException; + +} diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/auth/adapter/repository/package-info.java b/pay-mall-domain/src/main/java/edu/whut/domain/auth/adapter/repository/package-info.java new file mode 100644 index 0000000..d1bd72a --- /dev/null +++ b/pay-mall-domain/src/main/java/edu/whut/domain/auth/adapter/repository/package-info.java @@ -0,0 +1 @@ +package edu.whut.domain.auth.adapter.repository; \ No newline at end of file diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/auth/service/ILoginService.java b/pay-mall-domain/src/main/java/edu/whut/domain/auth/service/ILoginService.java new file mode 100644 index 0000000..4c6aa76 --- /dev/null +++ b/pay-mall-domain/src/main/java/edu/whut/domain/auth/service/ILoginService.java @@ -0,0 +1,14 @@ +package edu.whut.domain.auth.service; + +import java.io.IOException; + +public interface ILoginService { + + String createQrCodeTicket() throws Exception; + + String checkLogin(String ticket); + + + void saveLoginState(String ticket, String openid) throws IOException; + +} diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/auth/service/WeixinLoginService.java b/pay-mall-domain/src/main/java/edu/whut/domain/auth/service/WeixinLoginService.java new file mode 100644 index 0000000..55caa51 --- /dev/null +++ b/pay-mall-domain/src/main/java/edu/whut/domain/auth/service/WeixinLoginService.java @@ -0,0 +1,51 @@ +package edu.whut.domain.auth.service; +import com.google.common.cache.Cache; +import edu.whut.domain.auth.adapter.port.ILoginPort; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.IOException; + +@Slf4j +@Service +public class WeixinLoginService implements ILoginService { + + @Resource + private ILoginPort loginPort; + @Resource + private Cache openidToken; + + /** + * 生成登录二维码的 ticket + * 根据业务需求调用微信 API 或其他服务生成一个可用于前端展示的二维码凭证(ticket)。 + */ + @Override + public String createQrCodeTicket() throws Exception { + return loginPort.createQrCodeTicket(); + } + + /** + * 检查扫码登录状态 + * 根据前端传回的 ticket,轮询或查询登录结果, + * 如果用户已扫描并确认,则返回对应的 openid;否则返回 null 或空串。 + */ + @Override + public String checkLogin(String ticket) { + return openidToken.getIfPresent(ticket); + } + + /** + * 保存登录状态 + * 将用户的登录态(ticket 与 openid 的映射)持久化或缓存, + * 以便后续业务逻辑(如会话校验、权限验证等)使用。 + */ + @Override + public void saveLoginState(String ticket, String openid) throws IOException { + // 保存登录信息 + openidToken.put(ticket, openid); + // 发送模板消息 + loginPort.sendLoginTemplate(openid); + } + +} diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/adapter/repository/package-info.java b/pay-mall-domain/src/main/java/edu/whut/domain/yyy/adapter/repository/package-info.java deleted file mode 100644 index e69de29..0000000 diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/aggregate/package-info.java b/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/aggregate/package-info.java deleted file mode 100644 index a338f2b..0000000 --- a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/aggregate/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 聚合对象; - * 1. 聚合实体和值对象 - * 2. 聚合是聚合的对象,和提供基础处理对象的方法。但不建议在聚合中引入仓储和接口来做过大的逻辑。而这些复杂的操作应该放到service中处理 - * 3. 对象名称 XxxAggregate - */ -package edu.whut.domain.yyy.model.aggregate; \ No newline at end of file diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/entity/package-info.java b/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/entity/package-info.java deleted file mode 100644 index 1d9a9d6..0000000 --- a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/entity/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 实体对象; - * 1. 一般和数据库持久化对象1v1的关系,但因各自开发系统的不同,也有1vn的可能。 - * 2. 如果是老系统改造,那么旧的库表冗余了太多的字段,可能会有nv1的情况 - * 3. 对象名称 XxxEntity - */ -package edu.whut.domain.yyy.model.entity; \ No newline at end of file diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/valobj/package-info.java b/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/valobj/package-info.java deleted file mode 100644 index ecb5669..0000000 --- a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/model/valobj/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 值对象; - * 1. 用于描述对象属性的值,如一个库表中有json后者一个字段多个属性信息的枚举对象 - * 2. 对象名称如;XxxVO - */ -package edu.whut.domain.yyy.model.valobj; \ No newline at end of file diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/service/package-info.java b/pay-mall-domain/src/main/java/edu/whut/domain/yyy/service/package-info.java deleted file mode 100644 index ab79c10..0000000 --- a/pay-mall-domain/src/main/java/edu/whut/domain/yyy/service/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package edu.whut.domain.yyy.service; \ No newline at end of file diff --git a/pay-mall-infrastructure/pom.xml b/pay-mall-infrastructure/pom.xml index a6cbe6d..51919a5 100644 --- a/pay-mall-infrastructure/pom.xml +++ b/pay-mall-infrastructure/pom.xml @@ -18,6 +18,18 @@ org.projectlombok lombok + + com.squareup.retrofit2 + retrofit + + + com.squareup.retrofit2 + converter-jackson + + + com.squareup.retrofit2 + adapter-rxjava2 + edu.whut diff --git a/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/adapter/port/LoginPort.java b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/adapter/port/LoginPort.java new file mode 100644 index 0000000..00308e2 --- /dev/null +++ b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/adapter/port/LoginPort.java @@ -0,0 +1,98 @@ +package edu.whut.infrastructure.adapter.port; +import com.google.common.cache.Cache; +import edu.whut.domain.auth.adapter.port.ILoginPort; +import edu.whut.infrastructure.gateway.IWeixinApiService; +import edu.whut.infrastructure.gateway.dto.WeixinQrCodeRequestDTO; +import edu.whut.infrastructure.gateway.dto.WeixinQrCodeResponseDTO; +import edu.whut.infrastructure.gateway.dto.WeixinTemplateMessageDTO; +import edu.whut.infrastructure.gateway.dto.WeixinTokenResponseDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import retrofit2.Call; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Service +public class LoginPort implements ILoginPort { + + @Value("${weixin.config.app-id}") + private String appid; + @Value("${weixin.config.app-secret}") + private String appSecret; + @Value("${weixin.config.template_id}") + private String template_id; + + @Resource + private Cache weixinAccessToken; + + @Resource + private IWeixinApiService weixinApiService; + + /** + * 生成二维码登录凭证 ticket + * 获取或刷新 access_token 后,调用微信接口创建带参数的二维码,返回 ticket。 + */ + @Override + public String createQrCodeTicket() throws IOException { + // 1. 获取 access_token,优先从缓存读取,缓存失效时调用微信接口获取并更新缓存 + String accessToken = weixinAccessToken.getIfPresent(appid); + if (null == accessToken) { + Call call = weixinApiService.getToken("client_credential", appid, appSecret); + WeixinTokenResponseDTO weixinTokenRes = call.execute().body(); + assert weixinTokenRes != null; + accessToken = weixinTokenRes.getAccess_token(); + weixinAccessToken.put(appid, accessToken); + } + + // 2. 构造二维码请求对象,设置过期时间及业务参数 + WeixinQrCodeRequestDTO weixinQrCodeReq = WeixinQrCodeRequestDTO.builder() + .expire_seconds(2592000) + .action_name(WeixinQrCodeRequestDTO.ActionNameTypeVO.QR_SCENE.getCode()) + .action_info(WeixinQrCodeRequestDTO.ActionInfo.builder() + .scene(WeixinQrCodeRequestDTO.ActionInfo.Scene.builder() + .scene_id(100601) + .build()) + .build()) + .build(); + + // 3. 调用微信接口生成二维码 ticket + Call call = weixinApiService.createQrCode(accessToken, weixinQrCodeReq); + WeixinQrCodeResponseDTO weixinQrCodeRes = call.execute().body(); + assert null != weixinQrCodeRes; + return weixinQrCodeRes.getTicket(); + } + + /** + * 发送登录成功模板消息 + * 获取或刷新 access_token 后,封装模板数据,调用微信接口发送模板消息。 + */ + @Override + public void sendLoginTemplate(String openid) throws IOException { + // 1. 获取 accessToken 【实际业务场景,按需处理下异常】 + String accessToken = weixinAccessToken.getIfPresent(appid); + if (null == accessToken){ + Call call = weixinApiService.getToken("client_credential", appid, appSecret); + WeixinTokenResponseDTO weixinTokenRes = call.execute().body(); + assert weixinTokenRes != null; + accessToken = weixinTokenRes.getAccess_token(); + weixinAccessToken.put(appid, accessToken); + } + + // 2. 构造模板消息的 data 数据结构 + Map> data = new HashMap<>(); + WeixinTemplateMessageDTO.put(data, WeixinTemplateMessageDTO.TemplateKey.USER, openid); + + // 3. 构造模板消息对象 + WeixinTemplateMessageDTO templateMessageDTO = new WeixinTemplateMessageDTO(openid, template_id); + templateMessageDTO.setUrl("https://blog.bitday.top"); + templateMessageDTO.setData(data); + + // 4. 调用微信接口发送模板消息 + Call call = weixinApiService.sendMessage(accessToken, templateMessageDTO); + call.execute(); + } + +} diff --git a/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/IWeixinApiService.java b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/IWeixinApiService.java new file mode 100644 index 0000000..ffedd26 --- /dev/null +++ b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/IWeixinApiService.java @@ -0,0 +1,54 @@ +package edu.whut.infrastructure.gateway; +import edu.whut.infrastructure.gateway.dto.WeixinQrCodeRequestDTO; +import edu.whut.infrastructure.gateway.dto.WeixinQrCodeResponseDTO; +import edu.whut.infrastructure.gateway.dto.WeixinTemplateMessageDTO; +import edu.whut.infrastructure.gateway.dto.WeixinTokenResponseDTO; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Query; + +/** + * 微信API服务 retrofit2 + */ +public interface IWeixinApiService { + + /** + * 获取 Access token + * 文档:Get_access_token + * + * @param grantType 获取access_token填写client_credential + * @param appId 第三方用户唯一凭证 + * @param appSecret 第三方用户唯一凭证密钥,即appsecret + * @return 响应结果 + */ + @GET("cgi-bin/token") + Call getToken(@Query("grant_type") String grantType, + @Query("appid") String appId, + @Query("secret") String appSecret); + + /** + * 获取凭据 ticket + * 文档:Generating_a_Parametric_QR_Code + * 前端根据凭证展示二维码 + * + * @param accessToken getToken 获取的 token 信息 + * @param weixinQrCodeRequestDTO 入参对象 + * @return 应答结果 + */ + @POST("cgi-bin/qrcode/create") + Call createQrCode(@Query("access_token") String accessToken, @Body WeixinQrCodeRequestDTO weixinQrCodeRequestDTO); + + /** + * 发送微信公众号模板消息 + * 文档:https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl + * + * @param accessToken getToken 获取的 token 信息 + * @param weixinTemplateMessageDTO 入参对象 + * @return 应答结果 + */ + @POST("cgi-bin/message/template/send") + Call sendMessage(@Query("access_token") String accessToken, @Body WeixinTemplateMessageDTO weixinTemplateMessageDTO); + +} diff --git a/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinQrCodeRequestDTO.java b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinQrCodeRequestDTO.java new file mode 100644 index 0000000..6262cd0 --- /dev/null +++ b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinQrCodeRequestDTO.java @@ -0,0 +1,48 @@ +package edu.whut.infrastructure.gateway.dto; + +import lombok.*; + +/** + * @description 获取微信登录二维码请求对象 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class WeixinQrCodeRequestDTO { + + private int expire_seconds; + private String action_name; + private ActionInfo action_info; + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ActionInfo { + Scene scene; + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Scene { + int scene_id; + String scene_str; + } + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public enum ActionNameTypeVO { + QR_SCENE("QR_SCENE", "临时的整型参数值"), + QR_STR_SCENE("QR_STR_SCENE", "临时的字符串参数值"), + QR_LIMIT_SCENE("QR_LIMIT_SCENE", "永久的整型参数值"), + QR_LIMIT_STR_SCENE("QR_LIMIT_STR_SCENE", "永久的字符串参数值"); + + private String code; + private String info; + } + +} diff --git a/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinQrCodeResponseDTO.java b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinQrCodeResponseDTO.java new file mode 100644 index 0000000..daa87dc --- /dev/null +++ b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinQrCodeResponseDTO.java @@ -0,0 +1,15 @@ +package edu.whut.infrastructure.gateway.dto; + +import lombok.Data; + +/** + * 获取微信登录二维码响应对象 + */ +@Data +public class WeixinQrCodeResponseDTO { + + private String ticket; + private Long expire_seconds; + private String url; + +} diff --git a/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinTemplateMessageDTO.java b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinTemplateMessageDTO.java new file mode 100644 index 0000000..c3a63af --- /dev/null +++ b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinTemplateMessageDTO.java @@ -0,0 +1,104 @@ +package edu.whut.infrastructure.gateway.dto; + +import java.util.HashMap; +import java.util.Map; + +/** + * 微信模板消息 + */ +public class WeixinTemplateMessageDTO { + + private String touser = "or0Ab6ivwmypESVp_bYuk92T6SvU"; + private String template_id = "GLlAM-Q4jdgsktdNd35hnEbHVam2mwsW2YWuxDhpQkU"; + private String url = "https://weixin.qq.com"; + private Map> data = new HashMap<>(); + + public WeixinTemplateMessageDTO(String touser, String template_id) { + this.touser = touser; + this.template_id = template_id; + } + + public void put(TemplateKey key, String value) { + data.put(key.getCode(), new HashMap() { + private static final long serialVersionUID = 7092338402387318563L; + + { + put("value", value); + } + }); + } + + public static void put(Map> data, TemplateKey key, String value) { + data.put(key.getCode(), new HashMap() { + private static final long serialVersionUID = 7092338402387318563L; + + { + put("value", value); + } + }); + } + + + public enum TemplateKey { + USER("user","用户ID") + ; + + private String code; + private String desc; + + TemplateKey(String code, String desc) { + this.code = code; + this.desc = desc; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + } + + + public String getTouser() { + return touser; + } + + public void setTouser(String touser) { + this.touser = touser; + } + + public String getTemplate_id() { + return template_id; + } + + public void setTemplate_id(String template_id) { + this.template_id = template_id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Map> getData() { + return data; + } + + public void setData(Map> data) { + this.data = data; + } + +} diff --git a/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinTokenResponseDTO.java b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinTokenResponseDTO.java new file mode 100644 index 0000000..b0b8351 --- /dev/null +++ b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/WeixinTokenResponseDTO.java @@ -0,0 +1,16 @@ +package edu.whut.infrastructure.gateway.dto; + +import lombok.Data; + +/** + * 获取 Access token DTO 对象 + */ +@Data +public class WeixinTokenResponseDTO { + + private String access_token; + private int expires_in; + private String errcode; + private String errmsg; + +} diff --git a/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/package-info.java b/pay-mall-infrastructure/src/main/java/edu/whut/infrastructure/gateway/dto/package-info.java deleted file mode 100644 index e69de29..0000000 diff --git a/pay-mall-trigger/src/main/java/edu/whut/trigger/http/LoginController.java b/pay-mall-trigger/src/main/java/edu/whut/trigger/http/LoginController.java new file mode 100644 index 0000000..7aed0be --- /dev/null +++ b/pay-mall-trigger/src/main/java/edu/whut/trigger/http/LoginController.java @@ -0,0 +1,77 @@ +package edu.whut.trigger.http; +import edu.whut.api.IAuthService; +import edu.whut.api.response.Response; +import edu.whut.domain.auth.service.ILoginService; +import edu.whut.types.common.Constants; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +@Slf4j +@RestController() +@CrossOrigin("*") +@RequestMapping("/api/v1/login") +public class LoginController implements IAuthService { + + @Resource + private ILoginService loginService; + + /** + * 生成并返回一个微信扫码登录的凭证(ticket)。 + * 前端拿到 ticket 后,会用它来换取二维码图片并展示给用户。 + */ + @GetMapping("/weixin_qrcode_ticket") + @Override + public Response weixinQrCodeTicket() { + try { + String qrCodeTicket = loginService.createQrCodeTicket(); + log.info("生成微信扫码登录 ticket:{}", qrCodeTicket); + return Response.builder() + .code(Constants.ResponseCode.SUCCESS.getCode()) + .info(Constants.ResponseCode.SUCCESS.getInfo()) + .data(qrCodeTicket) + .build(); + } catch (Exception e) { + log.error("生成微信扫码登录 ticket 失败", e); + return Response.builder() + .code(Constants.ResponseCode.UN_ERROR.getCode()) + .info(Constants.ResponseCode.UN_ERROR.getInfo()) + .build(); + } + } + + /** + * 检测指定 ticket 的登录状态: + * 如果用户已扫码、后端已收到 openid 回调,就返回对应的登录令牌(如 openidToken 或 JWT)。 + * 否则返回 “未登录” 的状态码,前端可以继续轮询。 + */ + @GetMapping("/check_login") + @Override + public Response checkLogin(String ticket) { + try { + String openidToken = loginService.checkLogin(ticket); + log.info("扫码检测登录结果 ticket:{} openidToken:{}", ticket, openidToken); + if (StringUtils.isNotBlank(openidToken)) { + return Response.builder() + .code(Constants.ResponseCode.SUCCESS.getCode()) + .info(Constants.ResponseCode.SUCCESS.getInfo()) + .data(openidToken) + .build(); + } else { + return Response.builder() + .code(Constants.ResponseCode.NO_LOGIN.getCode()) + .info(Constants.ResponseCode.NO_LOGIN.getInfo()) + .build(); + } + } catch (Exception e) { + log.error("扫码检测登录结果失败 ticket:{}", ticket, e); + return Response.builder() + .code(Constants.ResponseCode.UN_ERROR.getCode()) + .info(Constants.ResponseCode.UN_ERROR.getInfo()) + .build(); + } + } + +} diff --git a/pay-mall-trigger/src/main/java/edu/whut/trigger/http/WeixinPortalController.java b/pay-mall-trigger/src/main/java/edu/whut/trigger/http/WeixinPortalController.java index e72cc43..be9498e 100644 --- a/pay-mall-trigger/src/main/java/edu/whut/trigger/http/WeixinPortalController.java +++ b/pay-mall-trigger/src/main/java/edu/whut/trigger/http/WeixinPortalController.java @@ -1,7 +1,9 @@ package edu.whut.trigger.http; +import edu.whut.domain.auth.service.ILoginService; import edu.whut.types.weixin.MessageTextEntity; import edu.whut.types.weixin.SignatureUtil; import edu.whut.types.weixin.XmlUtil; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; @@ -9,10 +11,12 @@ import org.springframework.web.bind.annotation.*; /** * https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index 平台地址 + * https://pay.bitday.top/api/v1/weixin/portal/receive 内网穿透 */ @Slf4j @RestController() @CrossOrigin("*") +@RequiredArgsConstructor @RequestMapping("/api/v1/weixin/portal/") public class WeixinPortalController { @@ -21,6 +25,15 @@ public class WeixinPortalController { @Value("${weixin.config.token}") private String token; + private final ILoginService loginService; + + //测试内网穿透 + @GetMapping(value = "test", produces = "text/plain;charset=UTF-8") + public String testFrp() { + log.info("内网穿透测试接口被调用"); + return "内网穿透测试成功"; + } + @GetMapping(value = "receive", produces = "text/plain;charset=utf-8") public String validate(@RequestParam(value = "signature", required = false) String signature, @RequestParam(value = "timestamp", required = false) String timestamp, @@ -43,6 +56,9 @@ public class WeixinPortalController { } } + /** + * 用户扫码登录会触发该函数 + */ @PostMapping(value = "receive", produces = "application/xml; charset=UTF-8") public String post(@RequestBody String requestBody, @RequestParam("signature") String signature, @@ -55,6 +71,12 @@ public class WeixinPortalController { log.info("接收微信公众号信息请求{}开始 {}", openid, requestBody); // 消息转换 MessageTextEntity message = XmlUtil.xmlToBean(requestBody, MessageTextEntity.class); + + if ("event".equals(message.getMsgType()) && "SCAN".equals(message.getEvent())) { + loginService.saveLoginState(message.getTicket(), openid); + return buildMessageTextEntity(openid, "登录成功"); + } + return buildMessageTextEntity(openid, "你好," + message.getContent()); } catch (Exception e) { log.error("接收微信公众号信息请求{}失败 {}", openid, requestBody, e); diff --git a/pay-mall-trigger/src/main/java/edu/whut/trigger/http/package-info.java b/pay-mall-trigger/src/main/java/edu/whut/trigger/http/package-info.java deleted file mode 100644 index 40cb8a7..0000000 --- a/pay-mall-trigger/src/main/java/edu/whut/trigger/http/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * HTTP 接口服务 - */ -package edu.whut.trigger.http; \ No newline at end of file diff --git a/pay-mall-types/src/main/java/edu/whut/types/common/Constants.java b/pay-mall-types/src/main/java/edu/whut/types/common/Constants.java index 381d140..a0c2b99 100644 --- a/pay-mall-types/src/main/java/edu/whut/types/common/Constants.java +++ b/pay-mall-types/src/main/java/edu/whut/types/common/Constants.java @@ -1,7 +1,24 @@ package edu.whut.types.common; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + public class Constants { public final static String SPLIT = ","; + @AllArgsConstructor + @NoArgsConstructor + @Getter + public enum ResponseCode { + SUCCESS("0000", "调用成功"), + UN_ERROR("0001", "调用失败"), + ILLEGAL_PARAMETER("0002", "非法参数"), + NO_LOGIN("0003", "未登录"), + ; + private String code; + private String info; + + } } diff --git a/pom.xml b/pom.xml index 93dc316..ad612de 100644 --- a/pom.xml +++ b/pom.xml @@ -115,6 +115,21 @@ commons-codec 1.15 + + com.squareup.retrofit2 + retrofit + 2.9.0 + + + com.squareup.retrofit2 + converter-jackson + 2.9.0 + + + com.squareup.retrofit2 + adapter-rxjava2 + 2.9.0 +