feat: 意见反馈支持登录与游客分流
可选 Bearer 写入 auth_type/uid,游客必填 contact;Admin 与 popadmin 可按类型筛选。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d5c0d81e22
commit
082f420347
@ -3133,26 +3133,54 @@ const pages = {
|
||||
},
|
||||
|
||||
async sparkFeedbacks() {
|
||||
const authType = ($('fbAuthType') && $('fbAuthType').value) || '';
|
||||
let q = `?page=0&size=50`;
|
||||
if (authType) q += `&auth_type=${encodeURIComponent(authType)}`;
|
||||
let data = { list: [], total: 0 };
|
||||
try { data = await api('/spark/feedbacks?page=0&size=50'); } catch (e) {
|
||||
try { data = await api('/spark/feedbacks' + q); } catch (e) {
|
||||
$('content').innerHTML = `<div class="note error">App反馈 API:${esc(e.message)}</div>`;
|
||||
return;
|
||||
}
|
||||
const list = data.list || [];
|
||||
const authLabel = (t) => t === 'login' ? '已登录' : (t === 'guest' ? '未登录' : (t || '-'));
|
||||
const identity = (f) => {
|
||||
if (f.auth_type === 'login' || (f.user_id != null && f.user_id !== '')) {
|
||||
return `uid ${esc(f.user_id??'')}`;
|
||||
}
|
||||
return esc(f.contact || f.email || '-');
|
||||
};
|
||||
$('content').innerHTML = `
|
||||
<div class="note">SleepHeart 意见反馈 · <code>/api/admin/spark/feedbacks</code> · 客户端 <code>POST /v1/feedback</code> · 共 ${esc(data.total||0)} 条</div>
|
||||
<div class="note">意见反馈 · <code>/api/admin/spark/feedbacks</code> · 客户端 <code>POST /v1/feedback</code>(可选 Bearer)· 共 ${esc(data.total||0)} 条</div>
|
||||
<div class="toolbar" style="margin:8px 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<label>登录状态
|
||||
<select id="fbAuthType">
|
||||
<option value="">全部</option>
|
||||
<option value="login"${authType==='login'?' selected':''}>已登录</option>
|
||||
<option value="guest"${authType==='guest'?' selected':''}>未登录</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" id="fbReload" class="secondary">刷新</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>内容</th><th>邮箱</th><th>语言</th><th>状态</th><th>时间</th><th></th></tr></thead>
|
||||
<thead><tr><th>ID</th><th>类型</th><th>身份/联系</th><th>内容</th><th>版本</th><th>语言</th><th>状态</th><th>时间</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
${list.length ? list.map((f)=>`<tr>
|
||||
<td>${esc(f.id)}</td><td style="max-width:280px">${esc(f.content)}</td><td>${esc(f.email||'')}</td>
|
||||
<td>${esc(f.language||'')}</td><td>${esc(f.status||'')}</td><td>${esc(f.created_at||'')}</td>
|
||||
<td>${esc(f.id)}</td>
|
||||
<td>${esc(authLabel(f.auth_type))}</td>
|
||||
<td>${identity(f)}</td>
|
||||
<td style="max-width:280px">${esc(f.content)}</td>
|
||||
<td>${esc(f.app_version||'')}</td>
|
||||
<td>${esc(f.language||'')}</td>
|
||||
<td>${esc(f.status||'')}</td>
|
||||
<td>${esc(f.created_at||'')}</td>
|
||||
<td>
|
||||
<button type="button" class="secondary btn-sm" data-fdone="${esc(f.id)}">完成</button>
|
||||
<button type="button" class="secondary btn-sm" data-fign="${esc(f.id)}">忽略</button>
|
||||
</td></tr>`).join('') : '<tr><td colspan="7">暂无反馈</td></tr>'}
|
||||
</td></tr>`).join('') : '<tr><td colspan="9">暂无反馈</td></tr>'}
|
||||
</tbody>
|
||||
</table>`;
|
||||
$('fbReload').onclick = () => pages.sparkFeedbacks();
|
||||
$('fbAuthType').onchange = () => pages.sparkFeedbacks();
|
||||
$('content').querySelectorAll('[data-fdone]').forEach((btn)=>{
|
||||
btn.onclick = async () => { await api('/spark/feedbacks/'+btn.dataset.fdone,{method:'PATCH',body:JSON.stringify({status:'done'})}); pages.sparkFeedbacks(); };
|
||||
});
|
||||
|
||||
@ -77,6 +77,7 @@
|
||||
|
||||
- Spark Messenger 全页面接入检查
|
||||
|
||||
<<<<<<< HEAD
|
||||
## 14:48 — 修复 make lint / make test
|
||||
|
||||
### 概要
|
||||
@ -120,3 +121,39 @@
|
||||
```bash
|
||||
make install-git-hooks # 本机只需一次
|
||||
```
|
||||
=======
|
||||
## 14:55 — 意见反馈区分登录 / 游客
|
||||
|
||||
### 概要
|
||||
|
||||
- `POST /v1/feedback` 改为可选鉴权:有效 Bearer → `auth_type=login` + `uid`;无 token → `auth_type=guest`,必填 `contact`(邮箱或手机)
|
||||
- 表增加 `auth_type` / `contact`;Admin list 支持 `?auth_type=login|guest`;popadmin App反馈页加筛选下拉
|
||||
- OpenAPI 同步说明
|
||||
|
||||
### 变更范围
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| 功能 | 反馈登录/游客分流 |
|
||||
| 数据库 | migration 0024 |
|
||||
| 管理端 | popadmin 筛选 |
|
||||
| 文档 | OpenAPI |
|
||||
|
||||
### 主要文件
|
||||
|
||||
- `migrations/0024_feedback_auth_type_contact.sql`
|
||||
- `internal/module/client/**` — entity / logic / repo
|
||||
- `internal/transport/http/auth.go` — `/v1/feedback` 可选鉴权
|
||||
- `internal/transport/http/handler/admin/client.go` — auth_type 筛选
|
||||
- `internal/transport/http/docs/openapi.yaml`
|
||||
- `deploy/popadmin/popadmin.js`
|
||||
|
||||
### 验证
|
||||
|
||||
- `make test`: skipped(按需求不跑 go test)
|
||||
- 需部署并执行 migration 后生效
|
||||
|
||||
### 关联
|
||||
|
||||
- 用户需求:意见反馈分登录与未登录
|
||||
>>>>>>> c844c07 (feat: 意见反馈支持登录与游客分流)
|
||||
|
||||
@ -22,11 +22,18 @@ type LegalBundle struct {
|
||||
Terms LegalDocument `json:"terms"`
|
||||
}
|
||||
|
||||
const (
|
||||
FeedbackAuthLogin = "login"
|
||||
FeedbackAuthGuest = "guest"
|
||||
)
|
||||
|
||||
// Feedback is one client feedback row.
|
||||
type Feedback struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID *int64 `json:"user_id,omitempty"`
|
||||
Email string `json:"email"`
|
||||
AuthType string `json:"auth_type"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Contact string `json:"contact,omitempty"`
|
||||
Content string `json:"content"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Language string `json:"language"`
|
||||
|
||||
@ -20,7 +20,7 @@ type Repository interface {
|
||||
GetLegal(ctx context.Context, lang string) (privacy, terms *entity.LegalDocument, err error)
|
||||
UpsertLegal(ctx context.Context, d *entity.LegalDocument) error
|
||||
InsertFeedback(ctx context.Context, f *entity.Feedback) error
|
||||
ListFeedback(ctx context.Context, page, size int) ([]entity.Feedback, int64, error)
|
||||
ListFeedback(ctx context.Context, page, size int, authType string) ([]entity.Feedback, int64, error)
|
||||
UpdateFeedbackStatus(ctx context.Context, id int64, status string) error
|
||||
ListPlans(ctx context.Context, lang string) ([]entity.SubscriptionPlan, error)
|
||||
ListPlansAdmin(ctx context.Context, lang string) ([]entity.SubscriptionPlan, error)
|
||||
@ -52,6 +52,7 @@ func NewService(repo Repository, users UserFinder, otp OTPLogin) *Service {
|
||||
func (s *Service) Ready() bool { return s != nil }
|
||||
|
||||
type FeedbackInput struct {
|
||||
Contact string `json:"contact" validate:"omitempty,max=255"`
|
||||
Email string `json:"email" validate:"omitempty,email" label:"field.email" errmsg:"email:validate.login.email_invalid"`
|
||||
Content string `json:"content" validate:"required,min=2,max=2000" label:"field.content" errmsg:"required:validate.required"`
|
||||
AppVersion string `json:"app_version" validate:"omitempty,max=64"`
|
||||
@ -99,17 +100,36 @@ func (s *Service) SubmitFeedback(ctx context.Context, in FeedbackInput) (*entity
|
||||
if err := validate.StructLang(reqctx.Lang(ctx), &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contact := strings.TrimSpace(in.Contact)
|
||||
if contact == "" {
|
||||
contact = strings.TrimSpace(in.Email)
|
||||
}
|
||||
email := strings.TrimSpace(strings.ToLower(in.Email))
|
||||
f := &entity.Feedback{
|
||||
Email: strings.TrimSpace(strings.ToLower(in.Email)),
|
||||
Content: strings.TrimSpace(in.Content),
|
||||
Email: email, Contact: contact, Content: strings.TrimSpace(in.Content),
|
||||
AppVersion: strings.TrimSpace(in.AppVersion),
|
||||
Language: normalizeLang(reqctx.Language(ctx)),
|
||||
Status: "open",
|
||||
CreatedAt: time.Now(),
|
||||
Language: normalizeLang(reqctx.Language(ctx)), Status: "open", CreatedAt: time.Now(),
|
||||
}
|
||||
if f.AppVersion == "" {
|
||||
if rc := reqctx.From(ctx); rc != nil {
|
||||
f.AppVersion = strings.TrimSpace(rc.AppVersion)
|
||||
}
|
||||
}
|
||||
if rc := reqctx.From(ctx); rc != nil && rc.UserID > 0 {
|
||||
uid := rc.UserID
|
||||
f.UserID = &uid
|
||||
f.AuthType = entity.FeedbackAuthLogin
|
||||
} else {
|
||||
f.AuthType = entity.FeedbackAuthGuest
|
||||
if contact == "" {
|
||||
return nil, errs.ErrInvalidArgument.WithMsg("游客反馈须填写 contact(邮箱或手机)")
|
||||
}
|
||||
if !looksLikeContact(contact) {
|
||||
return nil, errs.ErrInvalidArgument.WithMsg("contact 须为邮箱或手机号")
|
||||
}
|
||||
if email == "" && strings.Contains(contact, "@") {
|
||||
f.Email = strings.ToLower(contact)
|
||||
}
|
||||
}
|
||||
if s.repo != nil {
|
||||
if err := s.repo.InsertFeedback(ctx, f); err != nil {
|
||||
@ -121,11 +141,32 @@ func (s *Service) SubmitFeedback(ctx context.Context, in FeedbackInput) (*entity
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListFeedbackAdmin(ctx context.Context, page, size int) ([]entity.Feedback, int64, error) {
|
||||
func looksLikeContact(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(s, "@") {
|
||||
at := strings.IndexByte(s, '@')
|
||||
return at > 0 && at < len(s)-1
|
||||
}
|
||||
digits := 0
|
||||
for _, r := range s {
|
||||
if r >= '0' && r <= '9' {
|
||||
digits++
|
||||
} else if r != '+' && r != '-' && r != ' ' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return digits >= 7 && digits <= 15
|
||||
}
|
||||
|
||||
func (s *Service) ListFeedbackAdmin(ctx context.Context, page, size int, authType string) ([]entity.Feedback, int64, error) {
|
||||
if s.repo == nil {
|
||||
return nil, 0, nil
|
||||
}
|
||||
return s.repo.ListFeedback(ctx, page, size)
|
||||
authType = strings.TrimSpace(authType)
|
||||
return s.repo.ListFeedback(ctx, page, size, authType)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateFeedbackStatus(ctx context.Context, id int64, status string) error {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
<<<<<<< HEAD
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
@ -132,6 +133,12 @@ func (s stubOTP) AuthenticateEmail(context.Context, string, string, *userentity.
|
||||
func ctxLang(lang string) context.Context {
|
||||
return reqctx.With(context.Background(), &reqctx.RequestContext{Language: lang})
|
||||
}
|
||||
=======
|
||||
"testing"
|
||||
|
||||
"spark-server/internal/module/client/entity"
|
||||
)
|
||||
>>>>>>> c844c07 (feat: 意见反馈支持登录与游客分流)
|
||||
|
||||
func TestLegalPlansFeedbackFallback(t *testing.T) {
|
||||
svc := NewService(nil, nil, nil)
|
||||
@ -143,8 +150,8 @@ func TestLegalPlansFeedbackFallback(t *testing.T) {
|
||||
if err != nil || len(plans.Plans) != 4 {
|
||||
t.Fatalf("plans %+v err=%v", plans, err)
|
||||
}
|
||||
fb, err := svc.SubmitFeedback(t.Context(), FeedbackInput{Content: "界面很好", Email: "a@b.com"})
|
||||
if err != nil || fb.ID == 0 {
|
||||
fb, err := svc.SubmitFeedback(t.Context(), FeedbackInput{Content: "界面很好", Contact: "a@b.com"})
|
||||
if err != nil || fb.ID == 0 || fb.AuthType != entity.FeedbackAuthGuest {
|
||||
t.Fatalf("feedback %+v err=%v", fb, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,9 @@ func (legalRow) TableName() string { return "t_legal_documents" }
|
||||
type feedbackRow struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||
UserID *int64 `gorm:"column:user_id"`
|
||||
AuthType string `gorm:"column:auth_type"`
|
||||
Email string `gorm:"column:email"`
|
||||
Contact string `gorm:"column:contact"`
|
||||
Content string `gorm:"column:content"`
|
||||
AppVersion string `gorm:"column:app_version"`
|
||||
Language string `gorm:"column:language"`
|
||||
@ -110,31 +112,45 @@ func (r *Repo) InsertFeedback(ctx context.Context, f *entity.Feedback) error {
|
||||
if status == "" {
|
||||
status = "open"
|
||||
}
|
||||
authType := f.AuthType
|
||||
if authType == "" {
|
||||
authType = entity.FeedbackAuthGuest
|
||||
}
|
||||
row := feedbackRow{
|
||||
UserID: f.UserID, Email: f.Email, Content: f.Content,
|
||||
AppVersion: f.AppVersion, Language: f.Language, Status: status, CreatedAt: f.CreatedAt,
|
||||
UserID: f.UserID, AuthType: authType, Email: f.Email, Contact: f.Contact,
|
||||
Content: f.Content, AppVersion: f.AppVersion, Language: f.Language,
|
||||
Status: status, CreatedAt: f.CreatedAt,
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
f.ID = row.ID
|
||||
f.Status = status
|
||||
f.AuthType = authType
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) ListFeedback(ctx context.Context, page, size int) ([]entity.Feedback, int64, error) {
|
||||
func (r *Repo) ListFeedback(ctx context.Context, page, size int, authType string) ([]entity.Feedback, int64, error) {
|
||||
if page < 0 {
|
||||
page = 0
|
||||
}
|
||||
if size <= 0 || size > 200 {
|
||||
size = 50
|
||||
}
|
||||
q := r.db.WithContext(ctx).Model(&feedbackRow{})
|
||||
if authType == entity.FeedbackAuthLogin || authType == entity.FeedbackAuthGuest {
|
||||
q = q.Where("auth_type = ?", authType)
|
||||
}
|
||||
var total int64
|
||||
if err := r.db.WithContext(ctx).Model(&feedbackRow{}).Count(&total).Error; err != nil {
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var rows []feedbackRow
|
||||
err := r.db.WithContext(ctx).Order("id DESC").Offset(page * size).Limit(size).Find(&rows).Error
|
||||
listQ := r.db.WithContext(ctx).Order("id DESC").Offset(page * size).Limit(size)
|
||||
if authType == entity.FeedbackAuthLogin || authType == entity.FeedbackAuthGuest {
|
||||
listQ = listQ.Where("auth_type = ?", authType)
|
||||
}
|
||||
err := listQ.Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -142,8 +158,9 @@ func (r *Repo) ListFeedback(ctx context.Context, page, size int) ([]entity.Feedb
|
||||
for i := range rows {
|
||||
row := &rows[i]
|
||||
out = append(out, entity.Feedback{
|
||||
ID: row.ID, UserID: row.UserID, Email: row.Email, Content: row.Content,
|
||||
AppVersion: row.AppVersion, Language: row.Language, Status: row.Status, CreatedAt: row.CreatedAt,
|
||||
ID: row.ID, UserID: row.UserID, AuthType: row.AuthType, Email: row.Email,
|
||||
Contact: row.Contact, Content: row.Content, AppVersion: row.AppVersion,
|
||||
Language: row.Language, Status: row.Status, CreatedAt: row.CreatedAt,
|
||||
})
|
||||
}
|
||||
return out, total, nil
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
authlogic "spark-server/internal/module/auth/logic"
|
||||
authentity "spark-server/internal/module/auth/entity"
|
||||
"spark-server/internal/pkg/errs"
|
||||
"spark-server/internal/pkg/ratelimit"
|
||||
"spark-server/internal/pkg/reqctx"
|
||||
@ -27,7 +28,17 @@ const (
|
||||
|
||||
func Auth(svc authlogic.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if svc == nil || publicPath(c.Request.URL.Path) {
|
||||
if svc == nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if publicPath(c.Request.URL.Path) {
|
||||
if optionalAuthPath(c.Request.URL.Path) {
|
||||
if err := tryOptionalAuth(c, svc); err != nil {
|
||||
abortAuth(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@ -45,16 +56,7 @@ func Auth(svc authlogic.Service) gin.HandlerFunc {
|
||||
abortAuth(c, err)
|
||||
return
|
||||
}
|
||||
rc := reqctx.From(c.Request.Context())
|
||||
if rc == nil {
|
||||
rc = &reqctx.RequestContext{}
|
||||
c.Request = c.Request.WithContext(reqctx.With(c.Request.Context(), rc))
|
||||
}
|
||||
rc.UserID = principal.UserID
|
||||
rc.DeviceID = principal.DeviceID
|
||||
rc.AppID = principal.AppID
|
||||
rc.JTI = principal.JTI
|
||||
rc.TokenExpires = principal.ExpiresAt
|
||||
applyPrincipal(c, principal)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@ -78,7 +80,7 @@ func AuthRateLimit(l *ratelimit.Limiter) gin.HandlerFunc {
|
||||
func authLimitPath(path string) bool {
|
||||
switch path {
|
||||
case "/v1/auth/login", "/v1/auth/token/refresh", "/v1/sms/codes",
|
||||
"/v1/auth/password-login", "/v1/auth/password":
|
||||
"/v1/auth/password-login", "/v1/auth/password", "/v1/feedback":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@ -123,6 +125,48 @@ func matchDeviceHeader(raw string, want uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// optionalAuthPath accepts anonymous calls; Bearer if present must be valid.
|
||||
func optionalAuthPath(path string) bool {
|
||||
switch path {
|
||||
case "/v1/feedback", "/v1/sms/codes":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func tryOptionalAuth(c *gin.Context, svc authlogic.Service) error {
|
||||
token, ok := bearerToken(c.GetHeader(headerAuthorization))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
principal, err := svc.Authenticate(c.Request.Context(), token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := matchDeviceHeader(c.GetHeader(headerDeviceID), principal.DeviceID); err != nil {
|
||||
return err
|
||||
}
|
||||
applyPrincipal(c, principal)
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyPrincipal(c *gin.Context, principal *authentity.Principal) {
|
||||
if principal == nil {
|
||||
return
|
||||
}
|
||||
rc := reqctx.From(c.Request.Context())
|
||||
if rc == nil {
|
||||
rc = &reqctx.RequestContext{}
|
||||
c.Request = c.Request.WithContext(reqctx.With(c.Request.Context(), rc))
|
||||
}
|
||||
rc.UserID = principal.UserID
|
||||
rc.DeviceID = principal.DeviceID
|
||||
rc.AppID = principal.AppID
|
||||
rc.JTI = principal.JTI
|
||||
rc.TokenExpires = principal.ExpiresAt
|
||||
}
|
||||
|
||||
func abortAuth(c *gin.Context, err error) {
|
||||
c.Abort()
|
||||
if clientGone(c, err) {
|
||||
|
||||
@ -779,12 +779,15 @@ paths:
|
||||
post:
|
||||
tags: [client]
|
||||
security: []
|
||||
summary: 提交意见反馈
|
||||
summary: 提交意见反馈(登录可选)
|
||||
description: |
|
||||
可匿名提交。若带 Bearer 会关联 user_id。Body:`content` 必填;可选 `email`、`app_version`。
|
||||
可选鉴权:带有效 `Authorization: Bearer` → `auth_type=login` 并写入 `user_id`;
|
||||
无 token → `auth_type=guest`,Body 必填 `contact`(邮箱或手机)与 `content`。
|
||||
兼容字段 `email`(游客未传 contact 时可用 email 作为 contact)。
|
||||
operationId: submitFeedback
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/X-Language"
|
||||
- $ref: "#/components/parameters/X-Device-ID"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@ -794,11 +797,17 @@ paths:
|
||||
required: [content]
|
||||
properties:
|
||||
content: { type: string, minLength: 2, maxLength: 2000 }
|
||||
email: { type: string, format: email }
|
||||
contact:
|
||||
type: string
|
||||
description: 游客必填;邮箱或手机号
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 兼容旧字段;游客可代替 contact
|
||||
app_version: { type: string }
|
||||
responses:
|
||||
"200":
|
||||
description: data 为反馈记录(含 id)
|
||||
description: data 为反馈记录(含 id、auth_type、contact)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
||||
@ -3,6 +3,7 @@ package adminhandler
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@ -90,12 +91,13 @@ func (h *ClientHandler) ListFeedback(c *gin.Context) {
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "0"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "50"))
|
||||
list, total, err := h.svc.ListFeedbackAdmin(c.Request.Context(), page, size)
|
||||
authType := strings.TrimSpace(c.Query("auth_type"))
|
||||
list, total, err := h.svc.ListFeedbackAdmin(c.Request.Context(), page, size, authType)
|
||||
if err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
|
||||
OK(c, gin.H{"list": list, "total": total, "page": page, "size": size, "auth_type": authType})
|
||||
}
|
||||
|
||||
func (h *ClientHandler) PatchFeedback(c *gin.Context) {
|
||||
|
||||
13
migrations/0024_feedback_auth_type_contact.sql
Normal file
13
migrations/0024_feedback_auth_type_contact.sql
Normal file
@ -0,0 +1,13 @@
|
||||
-- 意见反馈区分登录 / 游客,并增加游客联系方式。
|
||||
|
||||
ALTER TABLE t_feedback
|
||||
ADD COLUMN auth_type VARCHAR(16) NOT NULL DEFAULT 'guest' COMMENT 'login|guest' AFTER user_id,
|
||||
ADD COLUMN contact VARCHAR(255) NOT NULL DEFAULT '' COMMENT '游客联系方式(邮箱或手机)' AFTER email;
|
||||
|
||||
-- 历史数据:有 user_id 视为登录,否则游客;联系方式回填 email。
|
||||
UPDATE t_feedback
|
||||
SET auth_type = CASE WHEN user_id IS NOT NULL AND user_id > 0 THEN 'login' ELSE 'guest' END,
|
||||
contact = CASE WHEN contact = '' AND email <> '' THEN email ELSE contact END;
|
||||
|
||||
ALTER TABLE t_feedback
|
||||
ADD KEY idx_feedback_auth_type (auth_type);
|
||||
Loading…
Reference in New Issue
Block a user