package app import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "time" ) func (a *App) availablePaymentChannels(ctx context.Context) []map[string]any { mode := a.configPlain(ctx, "payment.mode", "sandbox") gatewayReady := mode == "sandbox" || a.paymentGatewayConfigured(ctx) if a.config.Environment == "production" && mode == "sandbox" { gatewayReady = false } channels := []map[string]any{} if a.configBool(ctx, "payment.alipay.enabled", true) { channels = append(channels, map[string]any{"code": "alipay", "name": "支付宝", "icon": "支", "configured": gatewayReady}) } if a.configBool(ctx, "payment.wechat.enabled", true) { channels = append(channels, map[string]any{"code": "wechat", "name": "微信支付", "icon": "微", "configured": gatewayReady}) } return channels } func (a *App) paymentChannels(w http.ResponseWriter, r *http.Request) { reply(w, map[string]any{"mode": a.configPlain(r.Context(), "payment.mode", "sandbox"), "items": a.availablePaymentChannels(r.Context())}) } type planView struct { ID int64 `json:"id"` Code string `json:"code"` Name string `json:"name"` Level int `json:"level"` DurationDays int `json:"durationDays"` DailyActiveChatLimit int `json:"dailyActiveChatLimit"` PriceCent int `json:"priceCent"` OriginalPriceCent int `json:"originalPriceCent"` Status int `json:"status"` SortOrder int `json:"sortOrder"` } func (a *App) membershipPlans(w http.ResponseWriter, r *http.Request) { rows, err := a.db.QueryContext(r.Context(), `SELECT id,code,name,level,duration_days,daily_active_chat_limit,price_cent,original_price_cent,status,sort_order FROM membership_plans WHERE status=1 AND deleted_at IS NULL ORDER BY sort_order`) if err != nil { fail(w, 500, 50001, "查询套餐失败") return } defer rows.Close() items := []planView{} for rows.Next() { var item planView _ = rows.Scan(&item.ID, &item.Code, &item.Name, &item.Level, &item.DurationDays, &item.DailyActiveChatLimit, &item.PriceCent, &item.OriginalPriceCent, &item.Status, &item.SortOrder) items = append(items, item) } reply(w, map[string]any{"items": items}) } func (a *App) membershipStatus(w http.ResponseWriter, r *http.Request) { quota := a.dailyActiveChatQuota(r.Context(), current(r).ID) var planName string var level int var expires time.Time err := a.db.QueryRowContext(r.Context(), `SELECT p.name,p.level,s.expires_at FROM subscriptions s JOIN membership_plans p ON p.id=s.plan_id WHERE s.user_id=? AND s.status=1 AND s.started_at<=NOW(3) AND s.expires_at>NOW(3) ORDER BY p.level DESC,s.expires_at DESC LIMIT 1`, current(r).ID).Scan(&planName, &level, &expires) if err != nil { reply(w, map[string]any{"active": false, "dailyActiveChat": quota, "level": 0, "name": "普通用户"}) return } reply(w, map[string]any{"active": true, "dailyActiveChat": quota, "level": level, "name": planName, "expiresAt": expires}) } func (a *App) createOrder(w http.ResponseWriter, r *http.Request) { var req struct { PlanID int64 `json:"planId"` Channel string `json:"channel"` } if decode(r, &req) != nil || req.PlanID == 0 { fail(w, 400, 20001, "请选择套餐") return } channels := a.availablePaymentChannels(r.Context()) if req.Channel == "" && len(channels) > 0 { req.Channel, _ = channels[0]["code"].(string) } channelAllowed := false for _, channel := range channels { if channel["code"] == req.Channel { channelAllowed = channel["configured"] == true } } if !channelAllowed { fail(w, 400, 20001, "支付渠道未启用或配置不完整") return } var price int if a.db.QueryRowContext(r.Context(), `SELECT price_cent FROM membership_plans WHERE id=? AND status=1 AND deleted_at IS NULL`, req.PlanID).Scan(&price) != nil { fail(w, 404, 30001, "套餐不存在") return } orderNo := fmt.Sprintf("XY%d%d%s", time.Now().UnixMilli(), current(r).ID, randomToken()[:8]) result, err := a.db.ExecContext(r.Context(), `INSERT INTO orders(order_no,user_id,product_type,product_id,amount_cent,status,channel)VALUES(?,?,'membership',?,?,'CREATED',?)`, orderNo, current(r).ID, req.PlanID, price, req.Channel) if err != nil { fail(w, 500, 50001, "创建订单失败") return } id, _ := result.LastInsertId() reply(w, map[string]any{"id": id, "orderNo": orderNo, "amountCent": price, "status": "CREATED", "channel": req.Channel}) } func (a *App) payOrder(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { fail(w, 400, 20001, "订单编号无效") return } mode := a.configPlain(r.Context(), "payment.mode", "sandbox") if mode != "sandbox" && mode != "live" { fail(w, http.StatusServiceUnavailable, 50003, "支付模式配置无效") return } if mode == "live" { var orderNo, status, channel, subject, providerOrderNo, checkoutURL string var amountCent int var paymentPayload sql.NullString err = a.db.QueryRowContext(r.Context(), `SELECT o.order_no,o.amount_cent,o.status,o.channel,COALESCE(p.name,'会员套餐'),o.provider_order_no,o.checkout_url,o.payment_payload FROM orders o LEFT JOIN membership_plans p ON p.id=o.product_id WHERE o.id=? AND o.user_id=? AND o.deleted_at IS NULL`, id, current(r).ID).Scan(&orderNo, &amountCent, &status, &channel, &subject, &providerOrderNo, &checkoutURL, &paymentPayload) if err != nil { fail(w, http.StatusNotFound, 30001, "订单不存在") return } if status == "PAID" { reply(w, map[string]any{"success": true, "status": "PAID", "mode": mode}) return } if status != "CREATED" { fail(w, http.StatusBadRequest, 20001, "当前订单状态无法支付") return } if providerOrderNo != "" && (checkoutURL != "" || paymentPayload.Valid) { var appPayload map[string]any if paymentPayload.Valid && paymentPayload.String != "" { _ = json.Unmarshal([]byte(paymentPayload.String), &appPayload) } reply(w, map[string]any{"success": true, "mode": mode, "status": status, "providerOrderNo": providerOrderNo, "checkoutUrl": checkoutURL, "appPayload": appPayload}) return } gatewayResult, gatewayErr := a.createGatewayPayment(r.Context(), paymentGatewayOrder{OrderNo: orderNo, AmountCent: amountCent, Channel: channel, Subject: subject, UserID: current(r).ID}) if gatewayErr != nil { fail(w, http.StatusBadGateway, 50003, "支付网关下单失败") return } payloadJSON := "" if len(gatewayResult.AppPayload) > 0 { encoded, _ := json.Marshal(gatewayResult.AppPayload) payloadJSON = string(encoded) } if _, err = a.db.ExecContext(r.Context(), `UPDATE orders SET provider_order_no=?,checkout_url=?,payment_payload=? WHERE id=? AND user_id=? AND status='CREATED' AND provider_order_no=''`, gatewayResult.ProviderOrderNo, gatewayResult.CheckoutURL, payloadJSON, id, current(r).ID); err != nil { fail(w, http.StatusInternalServerError, 50001, "保存支付信息失败") return } reply(w, map[string]any{"success": true, "mode": mode, "status": status, "providerOrderNo": gatewayResult.ProviderOrderNo, "checkoutUrl": gatewayResult.CheckoutURL, "appPayload": gatewayResult.AppPayload}) return } if a.config.Environment == "production" { fail(w, http.StatusServiceUnavailable, 50003, "生产环境禁止沙箱支付") return } tx, err := a.db.BeginTx(r.Context(), nil) if err != nil { fail(w, 500, 50001, "支付失败") return } defer func() { _ = tx.Rollback() }() var userID, planID int64 var amountCent int var orderNo string var status string err = tx.QueryRowContext(r.Context(), `SELECT user_id,product_id,amount_cent,order_no,status FROM orders WHERE id=? AND user_id=? AND deleted_at IS NULL FOR UPDATE`, id, current(r).ID).Scan(&userID, &planID, &amountCent, &orderNo, &status) if err != nil { fail(w, 404, 30001, "订单不存在") return } if status == "PAID" { reply(w, map[string]any{"success": true, "status": "PAID"}) return } if status != "CREATED" { fail(w, 400, 20001, "当前订单状态无法支付") return } var durationDays, level int if err = tx.QueryRowContext(r.Context(), `SELECT duration_days,level FROM membership_plans WHERE id=? AND status=1 AND deleted_at IS NULL`, planID).Scan(&durationDays, &level); err != nil { fail(w, 400, 20001, "会员套餐已下架") return } if _, err = tx.ExecContext(r.Context(), `UPDATE orders SET status='PAID',paid_at=NOW(3),paid_amount_cent=?,provider_order_no=?,payment_notified_at=NOW(3) WHERE id=?`, amountCent, "sandbox:"+orderNo, id); err == nil { err = grantOrderMembershipTx(r.Context(), tx, id, userID, planID, durationDays, level) } if err != nil || tx.Commit() != nil { fail(w, 500, 50001, "支付入账失败") return } reply(w, map[string]any{"success": true, "status": "PAID", "mode": mode}) } func (a *App) orderStatus(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { fail(w, http.StatusBadRequest, 20001, "订单编号无效") return } var orderNo, status, channel string var amountCent int var paidAt sql.NullTime if err = a.db.QueryRowContext(r.Context(), `SELECT order_no,amount_cent,status,channel,paid_at FROM orders WHERE id=? AND user_id=? AND deleted_at IS NULL`, id, current(r).ID).Scan(&orderNo, &amountCent, &status, &channel, &paidAt); err != nil { fail(w, http.StatusNotFound, 30001, "订单不存在") return } result := map[string]any{"id": id, "orderNo": orderNo, "amountCent": amountCent, "status": status, "channel": channel} if paidAt.Valid { result["paidAt"] = paidAt.Time } reply(w, result) } func (a *App) myOrders(w http.ResponseWriter, r *http.Request) { rows, err := a.db.QueryContext(r.Context(), `SELECT o.id,o.order_no,o.amount_cent,o.status,o.channel,o.created_at,COALESCE(p.name,'已删除套餐') FROM orders o LEFT JOIN membership_plans p ON p.id=o.product_id WHERE o.user_id=? AND o.deleted_at IS NULL ORDER BY o.created_at DESC`, current(r).ID) if err != nil { fail(w, 500, 50001, "查询失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var id int64 var orderNo, status, channel, name string var amount int var created time.Time _ = rows.Scan(&id, &orderNo, &amount, &status, &channel, &created, &name) items = append(items, map[string]any{"id": id, "orderNo": orderNo, "amountCent": amount, "status": status, "channel": channel, "createdAt": created, "productName": name}) } reply(w, map[string]any{"items": items}) } func (a *App) notifications(w http.ResponseWriter, r *http.Request) { rows, err := a.db.QueryContext(r.Context(), `SELECT id,type,title,content,biz_type,biz_id,read_at,created_at FROM notifications WHERE user_id=? ORDER BY created_at DESC LIMIT 100`, current(r).ID) if err != nil { fail(w, 500, 50001, "查询失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var id int64 var typ, title, content, bizType string var bizID any var readAt any var created time.Time _ = rows.Scan(&id, &typ, &title, &content, &bizType, &bizID, &readAt, &created) items = append(items, map[string]any{"id": id, "type": typ, "title": title, "content": content, "bizType": bizType, "bizId": bizID, "readAt": readAt, "createdAt": created}) } reply(w, map[string]any{"items": items}) } func (a *App) readAllNotifications(w http.ResponseWriter, r *http.Request) { _, _ = a.db.ExecContext(r.Context(), `UPDATE notifications SET read_at=NOW(3) WHERE user_id=? AND read_at IS NULL`, current(r).ID) reply(w, map[string]bool{"success": true}) } func (a *App) appConfig(w http.ResponseWriter, r *http.Request) { rows, _ := a.db.QueryContext(r.Context(), `SELECT config_key,config_value,value_type FROM system_configs WHERE value_type<>'secret' AND config_key LIKE 'app.%'`) configs := map[string]any{} if rows != nil { defer rows.Close() for rows.Next() { var key, value, typ string _ = rows.Scan(&key, &value, &typ) configs[key] = value } } reply(w, map[string]any{"configs": configs, "features": map[string]bool{"nearby": true, "feed": true, "membership": true, "im": true}, "minVersion": "1.0.0"}) }