gengx
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (a *App) adminDeletePlan(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, 20001, "套餐编号无效")
|
||||
return
|
||||
}
|
||||
result, err := a.db.ExecContext(r.Context(), `UPDATE membership_plans SET status=0,deleted_at=NOW(3) WHERE id=? AND deleted_at IS NULL`, id)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, 50001, "删除套餐失败")
|
||||
return
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
fail(w, http.StatusNotFound, 30001, "套餐不存在或已删除")
|
||||
return
|
||||
}
|
||||
var subscriptions, orders int64
|
||||
_ = a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM subscriptions WHERE plan_id=?`, id).Scan(&subscriptions)
|
||||
_ = a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM orders WHERE product_id=?`, id).Scan(&orders)
|
||||
a.audit(r, "delete", "membership_plan", id, map[string]any{"mode": "soft", "subscriptions": subscriptions, "orders": orders})
|
||||
reply(w, map[string]any{"success": true, "archivedSubscriptions": subscriptions, "archivedOrders": orders})
|
||||
}
|
||||
|
||||
type adminOrderUpdateRequest struct {
|
||||
AmountCent *int `json:"amountCent"`
|
||||
Channel *string `json:"channel"`
|
||||
ProductID *int64 `json:"productId"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
func validOrderStatus(status string) bool {
|
||||
return status == "CREATED" || status == "PAID" || status == "REFUNDING" || status == "REFUNDED" || status == "CLOSED"
|
||||
}
|
||||
|
||||
func (a *App) syncEditedOrderEntitlement(r *http.Request, tx *sql.Tx, orderID, userID, oldPlanID, newPlanID int64, oldStatus, newStatus string) error {
|
||||
needsRevoke := oldStatus == "PAID" && (newStatus != "PAID" || oldPlanID != newPlanID)
|
||||
needsGrant := newStatus == "PAID" && (oldStatus != "PAID" || oldPlanID != newPlanID)
|
||||
if needsRevoke {
|
||||
if _, err := tx.ExecContext(r.Context(), `UPDATE subscriptions SET status=0 WHERE user_id=? AND status=1 AND source IN (?,?)`, userID, fmt.Sprintf("order:%d", orderID), fmt.Sprintf("admin_order:%d", orderID)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if needsGrant {
|
||||
var durationDays int
|
||||
if err := tx.QueryRowContext(r.Context(), `SELECT duration_days FROM membership_plans WHERE id=?`, newPlanID).Scan(&durationDays); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(r.Context(), `INSERT INTO subscriptions(user_id,plan_id,source,status,started_at,expires_at) VALUES(?,?,?,1,NOW(3),DATE_ADD(NOW(3),INTERVAL ? DAY))`, userID, newPlanID, fmt.Sprintf("admin_order:%d", orderID), durationDays); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if needsRevoke || needsGrant {
|
||||
return a.recomputeMembershipTx(r.Context(), tx, userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) adminUpdateOrder(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
var req adminOrderUpdateRequest
|
||||
if err != nil || decode(r, &req) != nil {
|
||||
fail(w, http.StatusBadRequest, 20001, "订单信息格式无效")
|
||||
return
|
||||
}
|
||||
tx, err := a.db.BeginTx(r.Context(), nil)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, 50001, "保存订单失败")
|
||||
return
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
var userID, oldPlanID int64
|
||||
var oldAmount int
|
||||
var oldStatus, oldChannel, providerOrderNo string
|
||||
if err = tx.QueryRowContext(r.Context(), `SELECT user_id,product_id,amount_cent,status,channel,provider_order_no FROM orders WHERE id=? AND deleted_at IS NULL FOR UPDATE`, id).Scan(&userID, &oldPlanID, &oldAmount, &oldStatus, &oldChannel, &providerOrderNo); err != nil {
|
||||
fail(w, http.StatusNotFound, 30001, "订单不存在或已删除")
|
||||
return
|
||||
}
|
||||
newPlanID, newAmount, newStatus, newChannel := oldPlanID, oldAmount, oldStatus, oldChannel
|
||||
if req.ProductID != nil {
|
||||
newPlanID = *req.ProductID
|
||||
}
|
||||
if req.AmountCent != nil {
|
||||
newAmount = *req.AmountCent
|
||||
}
|
||||
if req.Status != nil {
|
||||
newStatus = strings.ToUpper(strings.TrimSpace(*req.Status))
|
||||
}
|
||||
if req.Channel != nil {
|
||||
newChannel = strings.TrimSpace(*req.Channel)
|
||||
}
|
||||
if newPlanID <= 0 || newAmount < 0 || !validOrderStatus(newStatus) || len(newChannel) > 30 {
|
||||
fail(w, http.StatusBadRequest, 20001, "订单套餐、金额、渠道或状态无效")
|
||||
return
|
||||
}
|
||||
if a.configPlain(r.Context(), "payment.mode", "sandbox") == "live" {
|
||||
financialFieldsChanged := newPlanID != oldPlanID || newAmount != oldAmount || newChannel != oldChannel
|
||||
if financialFieldsChanged && (oldStatus != "CREATED" || providerOrderNo != "") {
|
||||
fail(w, http.StatusBadRequest, 20001, "生产订单创建支付流水后禁止修改套餐、金额或渠道")
|
||||
return
|
||||
}
|
||||
if newStatus != oldStatus && !(oldStatus == "CREATED" && newStatus == "CLOSED") {
|
||||
fail(w, http.StatusBadRequest, 20001, "生产订单的支付与退款状态只能由已验签回调更新")
|
||||
return
|
||||
}
|
||||
}
|
||||
var planExists int
|
||||
planQuery := `SELECT COUNT(*) FROM membership_plans WHERE id=?`
|
||||
if newStatus == "PAID" || newPlanID != oldPlanID {
|
||||
planQuery += ` AND deleted_at IS NULL`
|
||||
}
|
||||
if err = tx.QueryRowContext(r.Context(), planQuery, newPlanID).Scan(&planExists); err != nil || planExists == 0 {
|
||||
fail(w, http.StatusBadRequest, 20001, "选择的会员套餐不存在")
|
||||
return
|
||||
}
|
||||
if err = a.syncEditedOrderEntitlement(r, tx, id, userID, oldPlanID, newPlanID, oldStatus, newStatus); err == nil {
|
||||
_, err = tx.ExecContext(r.Context(), `UPDATE orders SET product_id=?,amount_cent=?,status=?,channel=?,paid_at=IF(?='PAID',COALESCE(paid_at,NOW(3)),paid_at) WHERE id=?`, newPlanID, newAmount, newStatus, newChannel, newStatus, id)
|
||||
}
|
||||
if err != nil || tx.Commit() != nil {
|
||||
fail(w, http.StatusInternalServerError, 50001, "保存订单及会员权益失败")
|
||||
return
|
||||
}
|
||||
a.audit(r, "update", "order", id, map[string]any{"previousStatus": oldStatus, "status": newStatus, "previousPlanId": oldPlanID, "planId": newPlanID, "amountCent": newAmount, "channel": newChannel})
|
||||
reply(w, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (a *App) adminDeleteOrder(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, 20001, "订单编号无效")
|
||||
return
|
||||
}
|
||||
var userID int64
|
||||
var status string
|
||||
if err = a.db.QueryRowContext(r.Context(), `SELECT user_id,status FROM orders WHERE id=? AND deleted_at IS NULL`, id).Scan(&userID, &status); err != nil {
|
||||
fail(w, http.StatusNotFound, 30001, "订单不存在或已删除")
|
||||
return
|
||||
}
|
||||
if status == "REFUNDING" {
|
||||
fail(w, http.StatusBadRequest, 20001, "退款处理中的订单不能删除")
|
||||
return
|
||||
}
|
||||
result, err := a.db.ExecContext(r.Context(), `UPDATE orders SET deleted_at=NOW(3) WHERE id=? AND deleted_at IS NULL`, id)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, 50001, "删除订单失败")
|
||||
return
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
fail(w, http.StatusNotFound, 30001, "订单不存在或已删除")
|
||||
return
|
||||
}
|
||||
a.audit(r, "delete", "order", id, map[string]any{"mode": "soft", "status": status, "userId": userID, "membershipPreserved": status == "PAID"})
|
||||
reply(w, map[string]any{"success": true, "membershipPreserved": status == "PAID"})
|
||||
}
|
||||
|
||||
func (a *App) adminMessages(w http.ResponseWriter, r *http.Request) {
|
||||
page, size, offset := pagination(r)
|
||||
keyword := strings.TrimSpace(r.URL.Query().Get("keyword"))
|
||||
conversationID, _ := strconv.ParseInt(strings.TrimSpace(r.URL.Query().Get("conversationId")), 10, 64)
|
||||
messageType, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("type")))
|
||||
where := ` WHERE 1=1`
|
||||
args := []any{}
|
||||
if conversationID > 0 {
|
||||
where += ` AND m.conversation_id=?`
|
||||
args = append(args, conversationID)
|
||||
}
|
||||
if messageType > 0 {
|
||||
where += ` AND m.message_type=?`
|
||||
args = append(args, messageType)
|
||||
}
|
||||
if keyword != "" {
|
||||
where += ` AND (CONVERT(m.client_msg_id USING utf8mb4) LIKE ? OR CAST(m.body AS CHAR CHARACTER SET utf8mb4) LIKE ? OR sp.nickname LIKE ? OR su.public_id LIKE ? OR EXISTS (SELECT 1 FROM im_conversation_members kcm JOIN users ku ON ku.id=kcm.user_id JOIN user_profiles kp ON kp.user_id=kcm.user_id WHERE kcm.conversation_id=m.conversation_id AND kcm.user_id<>m.sender_id AND (kp.nickname LIKE ? OR ku.public_id LIKE ?)))`
|
||||
like := "%" + keyword + "%"
|
||||
args = append(args, like, like, like, like, like, like)
|
||||
}
|
||||
base := ` FROM im_messages m JOIN users su ON su.id=m.sender_id JOIN user_profiles sp ON sp.user_id=m.sender_id`
|
||||
var total int64
|
||||
if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*)`+base+where, args...).Scan(&total); err != nil {
|
||||
fail(w, http.StatusInternalServerError, 50001, "查询消息记录失败")
|
||||
return
|
||||
}
|
||||
queryArgs := append(append([]any{}, args...), size, offset)
|
||||
rows, err := a.db.QueryContext(r.Context(), `SELECT m.id,m.conversation_id,m.seq,m.sender_id,su.public_id,sp.nickname,sp.avatar_url,m.client_msg_id,m.message_type,m.body,m.moderation_status,m.recalled_at,m.created_at,COALESCE((SELECT GROUP_CONCAT(CONCAT(kp.nickname,' (',ku.public_id,')') ORDER BY kp.nickname SEPARATOR '、') FROM im_conversation_members kcm JOIN users ku ON ku.id=kcm.user_id JOIN user_profiles kp ON kp.user_id=kcm.user_id WHERE kcm.conversation_id=m.conversation_id AND kcm.user_id<>m.sender_id),'')`+base+where+` ORDER BY m.created_at DESC,m.id DESC LIMIT ? OFFSET ?`, queryArgs...)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, 50001, "查询消息记录失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var id, convID, seq, senderID int64
|
||||
var publicID, nickname, avatar, clientMsgID, recipients string
|
||||
var typ, moderation int
|
||||
var body []byte
|
||||
var recalledAt sql.NullTime
|
||||
var createdAt time.Time
|
||||
if rows.Scan(&id, &convID, &seq, &senderID, &publicID, &nickname, &avatar, &clientMsgID, &typ, &body, &moderation, &recalledAt, &createdAt, &recipients) != nil {
|
||||
continue
|
||||
}
|
||||
var content any
|
||||
if json.Unmarshal(body, &content) != nil {
|
||||
content = string(body)
|
||||
}
|
||||
items = append(items, map[string]any{"id": id, "conversationId": convID, "seq": seq, "senderId": senderID, "senderPublicId": publicID, "senderNickname": nickname, "senderAvatar": avatar, "recipients": recipients, "clientMsgId": clientMsgID, "type": typ, "content": content, "moderationStatus": moderation, "recalledAt": nullableTime(recalledAt), "createdAt": createdAt})
|
||||
}
|
||||
reply(w, pageResult{Items: items, Total: total, Page: page, Size: size})
|
||||
}
|
||||
Reference in New Issue
Block a user