feat(api): 接入微信登录并原生实现咨询域(ECR-049/050)
小程序可在 Go 上完成微信手机号登录、测评、预约和下单,不再反代 Java。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,6 +22,7 @@ func (h *AuthHandler) Register(api *gin.RouterGroup) {
|
||||
g := api.Group("/auth")
|
||||
g.POST("/register", h.RegisterAccount)
|
||||
g.POST("/login", h.Login)
|
||||
g.POST("/wechat", h.WeChat)
|
||||
g.POST("/logout", h.Logout)
|
||||
g.GET("/me", h.Me)
|
||||
g.PATCH("/me", h.PatchMe)
|
||||
@@ -34,6 +35,12 @@ type authBody struct {
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
type wechatBody struct {
|
||||
Code string `json:"code"`
|
||||
EncryptedData string `json:"encryptedData"`
|
||||
IV string `json:"iv"`
|
||||
}
|
||||
|
||||
// RegisterAccount handles POST /auth/register (DeviceAuth required on group).
|
||||
func (h *AuthHandler) RegisterAccount(c *gin.Context) {
|
||||
userID, ok := middleware.UserIDFromContext(c)
|
||||
@@ -88,6 +95,32 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
response.OK(c, res)
|
||||
}
|
||||
|
||||
// WeChat handles POST /auth/wechat (mini-program phone login).
|
||||
func (h *AuthHandler) WeChat(c *gin.Context) {
|
||||
userID, ok := middleware.UserIDFromContext(c)
|
||||
if !ok {
|
||||
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
|
||||
return
|
||||
}
|
||||
var body wechatBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
|
||||
return
|
||||
}
|
||||
deviceKey := c.GetHeader(middleware.DeviceKeyHeader)
|
||||
res, err := h.Svc.WeChatLogin(c.Request.Context(), userID, deviceKey, body.Code, body.EncryptedData, body.IV)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrAccountRestricted) {
|
||||
response.Fail(c, http.StatusUnauthorized, 40113, err.Error())
|
||||
return
|
||||
}
|
||||
response.Fail(c, http.StatusBadRequest, 40115, err.Error())
|
||||
return
|
||||
}
|
||||
c.Set(string(middleware.UserIDKey), res.User.ID)
|
||||
response.OK(c, res)
|
||||
}
|
||||
|
||||
// Logout handles POST /auth/logout.
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
tok := bearerToken(c)
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/consult"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
|
||||
)
|
||||
|
||||
// ConsultHandler serves /api/v1/psychic/* (ECR-050).
|
||||
type ConsultHandler struct {
|
||||
Svc *consult.Service
|
||||
}
|
||||
|
||||
func (h *ConsultHandler) Register(api *gin.RouterGroup) {
|
||||
g := api.Group("/psychic")
|
||||
g.POST("/banner/all", h.banners)
|
||||
g.GET("/news/page", h.newsPage)
|
||||
g.GET("/news/get", h.newsGet)
|
||||
g.POST("/procotol/getByCode", h.protocol)
|
||||
g.POST("/study/list", h.studyList)
|
||||
g.POST("/study/getSingleDetail", h.studyDetail)
|
||||
g.POST("/user-choice/save", h.choiceSave)
|
||||
g.POST("/user-choice/getResult", h.choiceResult)
|
||||
g.POST("/user-choice/my-test", h.myTest)
|
||||
g.GET("/doctor-info/business-scope-list", h.scopes)
|
||||
g.GET("/doctor-info/page", h.doctorPage)
|
||||
g.GET("/doctor-info/get", h.doctorGet)
|
||||
g.POST("/doctor-info/focus", h.focusOn)
|
||||
g.POST("/doctor-info/cancel-focus", h.focusOff)
|
||||
g.GET("/doctor-info/get-show-info", h.showInfo)
|
||||
g.POST("/appointment/remain-list", h.remain)
|
||||
g.POST("/appointment/date-detail-list", h.dateDetail)
|
||||
g.POST("/order/create", h.orderCreate)
|
||||
g.GET("/order/get-pay-param", h.payParam)
|
||||
g.POST("/order/order-list", h.orderList)
|
||||
g.POST("/order/order-detail", h.orderDetail)
|
||||
g.POST("/order/cancel", h.orderCancel)
|
||||
g.POST("/order/delete-order", h.orderDelete)
|
||||
g.POST("/pay/createOrder", h.payDemo)
|
||||
g.POST("/pay/refund", h.payRefund)
|
||||
g.POST("/pay/notify", h.payNotify)
|
||||
g.GET("/platform-user/getSelfInfo", h.selfInfo)
|
||||
g.POST("/platform-user/update", h.updateSelf)
|
||||
g.GET("/platform-user/not-read-num", h.notRead)
|
||||
g.POST("/platform-user/focus-to-read", h.focusRead)
|
||||
g.POST("/platform-user/order-to-read", h.orderRead)
|
||||
g.POST("/platform-user/focus-all", h.focusAll)
|
||||
g.POST("/platform-user/user-feedback", h.feedback)
|
||||
g.POST("/platform-user/feedback-flag", h.feedbackFlag)
|
||||
}
|
||||
|
||||
func (h *ConsultHandler) banners(c *gin.Context) {
|
||||
data, err := h.Svc.Banners(c.Request.Context())
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) newsPage(c *gin.Context) {
|
||||
data, err := h.Svc.NewsPage(c.Request.Context(), qInt(c, "type", 0), qInt(c, "showMain", -1), qInt(c, "pageNo", 1), qInt(c, "pageSize", 20))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
|
||||
func (h *ConsultHandler) ok(c *gin.Context, data any, err error) {
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40001, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, data)
|
||||
}
|
||||
|
||||
func (h *ConsultHandler) newsGet(c *gin.Context) {
|
||||
data, err := h.Svc.NewsGet(c.Request.Context(), qInt64(c, "id"))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) protocol(c *gin.Context) {
|
||||
data, err := h.Svc.ProtocolByCode(c.Request.Context(), c.Query("code"))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) studyList(c *gin.Context) {
|
||||
data, err := h.Svc.TestList(c.Request.Context(), qInt(c, "showMain", -1))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) studyDetail(c *gin.Context) {
|
||||
data, err := h.Svc.TestDetail(c.Request.Context(), qInt64(c, "studyId"))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) choiceSave(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var in consult.ChoiceIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.SaveChoice(c.Request.Context(), uid, in)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) choiceResult(c *gin.Context) {
|
||||
data, err := h.Svc.GetResult(c.Request.Context(), qInt64(c, "resultId"))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) myTest(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
PageNo int `json:"pageNo"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&p)
|
||||
data, err := h.Svc.MyTests(c.Request.Context(), uid, p.PageNo, p.PageSize)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) scopes(c *gin.Context) {
|
||||
data, err := h.Svc.Scopes(c.Request.Context())
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) doctorPage(c *gin.Context) {
|
||||
data, err := h.Svc.DoctorPage(c.Request.Context(), c.Query("businessScope"), qInt(c, "isTop", -1), qInt(c, "pageNo", 1), qInt(c, "pageSize", 20))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) doctorGet(c *gin.Context) {
|
||||
uid, _ := middleware.UserIDFromContext(c)
|
||||
data, err := h.Svc.DoctorGet(c.Request.Context(), qInt64(c, "id"), uid)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) focusOn(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.Svc.SetFocus(c.Request.Context(), uid, qInt64(c, "id"), true); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40001, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) focusOff(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.Svc.SetFocus(c.Request.Context(), uid, qInt64(c, "id"), false); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40001, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) showInfo(c *gin.Context) {
|
||||
uid, _ := middleware.UserIDFromContext(c)
|
||||
data, err := h.Svc.ShowInfo(c.Request.Context(), qInt64(c, "id"), uid)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) remain(c *gin.Context) {
|
||||
var in struct {
|
||||
DoctorID int64 `json:"doctorId"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&in)
|
||||
data, err := h.Svc.RemainList(c.Request.Context(), in.DoctorID)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) dateDetail(c *gin.Context) {
|
||||
var in struct {
|
||||
DoctorID int64 `json:"doctorId"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&in)
|
||||
data, err := h.Svc.DateDetail(c.Request.Context(), in.DoctorID)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) orderCreate(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var in consult.OrderIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.CreateOrder(c.Request.Context(), uid, in)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) payParam(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.PayParam(c.Request.Context(), uid, c.Query("orderSn"))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) orderList(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
PageNo int `json:"pageNo"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&p)
|
||||
data, err := h.Svc.OrderList(c.Request.Context(), uid, p.PageNo, p.PageSize)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) orderDetail(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.OrderDetail(c.Request.Context(), uid, qInt64(c, "orderId"))
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) orderCancel(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.Svc.CancelOrder(c.Request.Context(), uid, qInt64(c, "orderId")); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40001, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) orderDelete(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.Svc.DeleteOrder(c.Request.Context(), uid, qInt64(c, "orderId")); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40001, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) payDemo(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.DemoPay(c.Request.Context(), uid)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) payRefund(c *gin.Context) {
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) payNotify(c *gin.Context) {
|
||||
raw, _ := c.GetRawData()
|
||||
sn := extractXML(string(raw), "out_trade_no")
|
||||
if sn != "" {
|
||||
_ = h.Svc.MarkPaid(c.Request.Context(), sn)
|
||||
}
|
||||
c.String(http.StatusOK, "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>")
|
||||
}
|
||||
func (h *ConsultHandler) selfInfo(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.SelfInfo(c.Request.Context(), uid)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) updateSelf(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
NickName string `json:"nickName"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
StayPeriod string `json:"stayPeriod"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&in)
|
||||
if err := h.Svc.UpdateSelf(c.Request.Context(), uid, in.NickName, in.AvatarUrl, in.StayPeriod); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40001, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) notRead(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.NotRead(c.Request.Context(), uid)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) focusRead(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = h.Svc.FocusToRead(c.Request.Context(), uid)
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) orderRead(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = h.Svc.OrderToRead(c.Request.Context(), uid)
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) focusAll(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.FocusAll(c.Request.Context(), uid)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
func (h *ConsultHandler) feedback(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
ContentText string `json:"contentText"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&in)
|
||||
if err := h.Svc.Feedback(c.Request.Context(), uid, in.ContentText); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40001, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, true)
|
||||
}
|
||||
func (h *ConsultHandler) feedbackFlag(c *gin.Context) {
|
||||
uid, ok := h.mustUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := h.Svc.FeedbackFlag(c.Request.Context(), uid)
|
||||
h.ok(c, data, err)
|
||||
}
|
||||
|
||||
func (h *ConsultHandler) mustUser(c *gin.Context) (uuid.UUID, bool) {
|
||||
uid, ok := middleware.UserIDFromContext(c)
|
||||
if !ok || !h.Svc.Registered(c.Request.Context(), uid) {
|
||||
response.Fail(c, http.StatusUnauthorized, 40112, "请先登录后再使用")
|
||||
return uuid.Nil, false
|
||||
}
|
||||
return uid, true
|
||||
}
|
||||
|
||||
func qInt(c *gin.Context, key string, def int) int {
|
||||
s := c.Query(key)
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func qInt64(c *gin.Context, key string) int64 {
|
||||
n, _ := strconv.ParseInt(c.Query(key), 10, 64)
|
||||
return n
|
||||
}
|
||||
|
||||
func extractXML(s, tag string) string {
|
||||
open, close := "<"+tag+">", "</"+tag+">"
|
||||
i := strings.Index(s, open)
|
||||
if i < 0 {
|
||||
open, close = "<"+tag+"><![CDATA[", "]]></"+tag+">"
|
||||
i = strings.Index(s, open)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
i += len(open)
|
||||
j := strings.Index(s[i:], close)
|
||||
if j < 0 {
|
||||
return ""
|
||||
}
|
||||
return s[i : i+j]
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/avatar"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
|
||||
)
|
||||
|
||||
// MediaHandler serves uploaded media files.
|
||||
@@ -17,6 +23,8 @@ type MediaHandler struct {
|
||||
// Register mounts public media routes.
|
||||
func (h *MediaHandler) Register(api *gin.RouterGroup) {
|
||||
api.GET("/media/avatars/:file", h.ServeAvatar)
|
||||
api.GET("/media/uploads/:file", h.ServeUpload)
|
||||
api.POST("/upload", h.Upload)
|
||||
}
|
||||
|
||||
// ServeAvatar streams a stored avatar image.
|
||||
@@ -34,3 +42,54 @@ func (h *MediaHandler) ServeAvatar(c *gin.Context) {
|
||||
c.File(full)
|
||||
_ = filepath.Ext(full)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) uploadDir() string {
|
||||
return "data/uploads"
|
||||
}
|
||||
|
||||
func (h *MediaHandler) ServeUpload(c *gin.Context) {
|
||||
name := filepath.Base(c.Param("file"))
|
||||
full := filepath.Join(h.uploadDir(), name)
|
||||
if _, err := os.Stat(full); err != nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.File(full)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) Upload(c *gin.Context) {
|
||||
fh, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 10000, "请选择文件")
|
||||
return
|
||||
}
|
||||
src, err := fh.Open()
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 10000, "读取失败")
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
ext := strings.ToLower(filepath.Ext(fh.Filename))
|
||||
if ext == "" {
|
||||
ext = ".jpg"
|
||||
}
|
||||
var b [8]byte
|
||||
_, _ = rand.Read(b[:])
|
||||
name := hex.EncodeToString(b[:]) + ext
|
||||
if err := os.MkdirAll(h.uploadDir(), 0o755); err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 50001, "保存失败")
|
||||
return
|
||||
}
|
||||
dstPath := filepath.Join(h.uploadDir(), name)
|
||||
dst, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 50001, "保存失败")
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 50001, "保存失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, "/api/v1/media/uploads/"+name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user