确定友商登陆逻辑
This commit is contained in:
parent
17bc00a42f
commit
a2de599f81
79
doc/partner.md
Normal file
79
doc/partner.md
Normal file
@ -0,0 +1,79 @@
|
||||
# 金蚕游戏管理后台相关说明
|
||||
|
||||
## 修改记录
|
||||
|
||||
|
||||
|
||||
## 说明
|
||||
|
||||
1. 所有请求参数中带*号的不能为空
|
||||
2. 如无特殊说明, 所有接口返回json, 顶级结构如下, 接口Response的数据结构说明只包含data部分
|
||||
|
||||
``` JSON
|
||||
{
|
||||
"code": 0, //0:成功 2: 缺少必要参数(accountid, sessionid) 4: 帐号被封, 5: 帐号未找到 100: 所有未定义的错误
|
||||
"msg": "", //错误描述, 一般在code=0时, 该字段为空
|
||||
"data": {}, // 数据
|
||||
}
|
||||
```
|
||||
|
||||
2. 页面列表
|
||||
|
||||
> 所有的页面均可单独调用, url: https://puzzle-admin.kingsome.cn/页面url?token=token&mini=1
|
||||
> token为 1号接口获取
|
||||
|
||||
| 页面名称 | 页面url |
|
||||
| ---------- | ----------------- |
|
||||
| 店铺管理员 | /#/shop/shopadmin |
|
||||
| 游戏设置 | /#/shop/setting |
|
||||
| 活动列表 | /#/shop/activity_list |
|
||||
| 挑战活动列表 | /#/shop/exam_list |
|
||||
| 抽奖转盘 | /#/shop/lottery_setting |
|
||||
| 分享设置 | /#/shop/share_setting |
|
||||
| 优惠券设置 | /#/marketing/coupon |
|
||||
| 题库设置 | /#/question/setting |
|
||||
| 自定义题库 | /#/question/shoppuzzles |
|
||||
| 邮件 | 暂不开放 |
|
||||
| 公告 | 暂不开放 |
|
||||
|
||||
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 获取token
|
||||
|
||||
1. Method: POST
|
||||
2. URI: /api/partner/login
|
||||
|
||||
> POST参数
|
||||
|
||||
|
||||
| 字段 | 说明 |
|
||||
| -------- | -------------------------------------- |
|
||||
| name | *店铺完整名称 |
|
||||
| sname | 店铺显示的短名称(如果该字段为空的话, 取name字段) |
|
||||
| sid | *唯一的店铺id, 接口根据此字段判断该次上报是否是同一家店铺 |
|
||||
| logo | 店铺的logo, 一个可以外网访问的url地址 |
|
||||
| timestamp | *10或13位均可 |
|
||||
| sign | *签名 |
|
||||
|
||||
> 签名字段说明:
|
||||
>
|
||||
> 取name,sid, timestamp和我们提供的SecretKey字段拼接成 name=店铺名称&sid=店铺id:timestamp:SecretKey, 取该字符串的sha1
|
||||
|
||||
```js
|
||||
let signStr = `name=${name}&sid=${sid}:${timestamp}:${secretKey}`
|
||||
let sha1sum = crypto.createHash('sha1')
|
||||
sha1sum.update(signStr)
|
||||
let sign = sha1sum.digest('hex')
|
||||
```
|
||||
|
||||
|
||||
3. Response: JSON
|
||||
|
||||
```js
|
||||
{
|
||||
token: '1231231aasa'
|
||||
}
|
||||
```
|
||||
|
@ -2,11 +2,12 @@ import BaseController from '../../common/base.controller'
|
||||
import { permission, role, router } from '../../decorators/router'
|
||||
import { ZError } from '../../common/ZError'
|
||||
import { Game } from '../../models/content/Game'
|
||||
import { generateQrFile } from '../../services/File'
|
||||
import { downloadRemoteFile, generateQrFile } from '../../services/File'
|
||||
import { getInviteeNum } from '../../services/JCFW'
|
||||
import { UserItem } from '../../models/user/UserItem'
|
||||
import { checkText } from '../../services/Baidu'
|
||||
import { msgSecCheck, refreshToken } from '../../services/Wechat'
|
||||
import { uploadToCDN } from '../../services/TencentCDN'
|
||||
|
||||
class GameController extends BaseController {
|
||||
@role('anon')
|
||||
@ -27,8 +28,12 @@ class GameController extends BaseController {
|
||||
// // return { token }
|
||||
// const { data } = await msgSecCheck(txt)
|
||||
// return { baidu: res.data, wx: data }
|
||||
return {}
|
||||
let { url } = req.params
|
||||
let path = (await downloadRemoteFile(url)) as string
|
||||
let { data } = await uploadToCDN(path, 'test')
|
||||
return data
|
||||
}
|
||||
|
||||
@permission(['game:read', 'shop:game_setting'])
|
||||
@router('post /api/games')
|
||||
async list(req, res) {
|
||||
|
70
src/admin/controllers/partner.controller.ts
Normal file
70
src/admin/controllers/partner.controller.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import BaseController from '../../common/base.controller'
|
||||
import { router } from '../../decorators/router'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { ZError } from '../../common/ZError'
|
||||
import { checkSign, createSign } from '../../utils/security.util'
|
||||
import { Shop } from '../../models/shop/Shop'
|
||||
import { Admin } from '../../models/admin/Admin'
|
||||
import { Game } from '../../models/content/Game'
|
||||
import { downloadRemoteFile } from '../../services/File'
|
||||
import { uploadToCDN } from '../../services/TencentCDN'
|
||||
|
||||
const SECRET_KEY = '37284c327e10d8b73cf4325f33a3de4b34032e3e'
|
||||
const passwd = customAlphabet('2345678abcdefghjkmnpqrstwxy', 10)
|
||||
|
||||
class PartnerController extends BaseController {
|
||||
/**
|
||||
* 店铺名
|
||||
* 店铺短名字
|
||||
* logo?
|
||||
* @param req
|
||||
* @param res
|
||||
* @return {Promise<{}>}
|
||||
*/
|
||||
@router('post /api/partner/login')
|
||||
async login(req: any, res: any) {
|
||||
let { name, sname, sid, logo, timestamp, sign } = req.params
|
||||
if (!name || !sid || !timestamp || !sign) {
|
||||
throw new ZError(10, '缺少必要参数')
|
||||
}
|
||||
const signKeys = ['name', 'sid']
|
||||
if (!checkSign({ secretKey: SECRET_KEY, data: req.params, timestamp, sign, signKeys })) {
|
||||
throw new ZError(21, 'sign error')
|
||||
}
|
||||
sname = sname || name
|
||||
const shopData = { name, showName: sname, source: 1, publish: true }
|
||||
const shop = await Shop.insertOrUpdate({ partnerId: sid }, shopData)
|
||||
// 保存一下, 以生成sid和numid
|
||||
if (shop.newRecord) {
|
||||
let game = await Game.findAvaOne()
|
||||
if (game) {
|
||||
shop.gameInfo = { gameid: game.id, versionid: game.versions[0]._id + '' }
|
||||
}
|
||||
}
|
||||
if (shop.logoPartner !== logo) {
|
||||
shop.logoPartner = logo
|
||||
setImmediate(async () => {
|
||||
let path = (await downloadRemoteFile(logo)) as string
|
||||
let { data } = await uploadToCDN(path, 'shop')
|
||||
console.log(`upload logo finished`, data)
|
||||
if (!data.errcode) {
|
||||
shop.logo = data.url_cdn
|
||||
await shop.save()
|
||||
account.avatar = data.url_cdn
|
||||
await account.save()
|
||||
}
|
||||
})
|
||||
}
|
||||
await shop.save()
|
||||
|
||||
const username = sid
|
||||
let accountData = { level: 9, roles: ['shopadmin'], department: shop.id, showname: sname }
|
||||
const account = await Admin.insertOrUpdate({ username }, accountData)
|
||||
if (account.newRecord) {
|
||||
account.updatePassword(shop.sid)
|
||||
await account.save()
|
||||
}
|
||||
const token = await res.jwtSign({ id: account.id })
|
||||
return { token }
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { getModelForClass, index, modelOptions, prop, ReturnModelType } from '@typegoose/typegoose'
|
||||
import { getModelForClass, index, modelOptions, pre, prop, ReturnModelType } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
|
||||
@ -35,6 +35,9 @@ export interface AdminClass extends Base, TimeStamps {}
|
||||
@dbconn()
|
||||
@index({ username: 1 }, { unique: true })
|
||||
@modelOptions({ schemaOptions: { collection: 'admin_user', timestamps: true } })
|
||||
@pre<AdminClass>('save', function () {
|
||||
this.newRecord = false
|
||||
})
|
||||
// @ts-ignore
|
||||
class AdminClass extends BaseModule {
|
||||
@prop({ maxlength: 20, minlength: 5, required: true })
|
||||
@ -86,6 +89,12 @@ class AdminClass extends BaseModule {
|
||||
*/
|
||||
@prop()
|
||||
public level: number
|
||||
/**
|
||||
* 是否为新记录, 只用于判断insertOrUpdate返回的记录是否为新纪录
|
||||
* @type {boolean}
|
||||
*/
|
||||
@prop({ default: true })
|
||||
public newRecord: boolean
|
||||
|
||||
public static async findByName(this: ReturnModelType<typeof AdminClass>, username) {
|
||||
return this.findOne({ username, deleted: false }).exec()
|
||||
|
@ -20,8 +20,10 @@ class GameInfo {
|
||||
@dbconn()
|
||||
@index({ location: '2dsphere' })
|
||||
@index({ sid: 1 }, { unique: true })
|
||||
@index({ partnerId: 1 }, { unique: false })
|
||||
@modelOptions({ schemaOptions: { collection: 'shop', timestamps: true } })
|
||||
@pre<ShopClass>('save', async function () {
|
||||
this.newRecord = false
|
||||
if (!this.sid) {
|
||||
let sid = ''
|
||||
while (!sid) {
|
||||
@ -46,6 +48,13 @@ class ShopClass extends BaseModule {
|
||||
@prop()
|
||||
public numid: number
|
||||
|
||||
/**
|
||||
* 从合作伙伴那来的唯一id
|
||||
* @type {string}
|
||||
*/
|
||||
@prop()
|
||||
public partnerId: string
|
||||
|
||||
@prop({ required: true })
|
||||
public name!: string
|
||||
/**
|
||||
@ -128,6 +137,18 @@ class ShopClass extends BaseModule {
|
||||
*/
|
||||
@prop()
|
||||
public logo: string
|
||||
/**
|
||||
* 合作伙伴那来的logo原始信息
|
||||
* @type {string}
|
||||
*/
|
||||
@prop()
|
||||
public logoPartner: string
|
||||
/**
|
||||
* 资料来源
|
||||
* @type {number} 0: 后台添加, 1: 合作伙伴接口添加
|
||||
*/
|
||||
@prop({ default: 0 })
|
||||
public source: number
|
||||
/**
|
||||
* 店铺的行业分类
|
||||
* @type {string}
|
||||
@ -153,6 +174,13 @@ class ShopClass extends BaseModule {
|
||||
@prop({ type: () => [String] })
|
||||
public qtypes: string[]
|
||||
|
||||
/**
|
||||
* 是否为新记录, 只用于判断insertOrUpdate返回的记录是否为新纪录
|
||||
* @type {boolean}
|
||||
*/
|
||||
@prop({ default: true })
|
||||
public newRecord: boolean
|
||||
|
||||
public static parseQueryParam(params) {
|
||||
let options: any = {
|
||||
opt: { deleted: false, show: true, publish: true },
|
||||
|
@ -3,7 +3,13 @@ import * as jetpack from 'fs-jetpack'
|
||||
import { Game } from '../models/content/Game'
|
||||
import { ZError } from '../common/ZError'
|
||||
import { generateQr } from './Wechat'
|
||||
import nodeHtmlToImage, { NodeHtmlToImageOptions } from 'node-html-to-image'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import nodeHtmlToImage from 'node-html-to-image'
|
||||
import axios from 'axios'
|
||||
import * as http from 'http'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 26)
|
||||
|
||||
export function generateUploadPath(subPath: string) {
|
||||
const base = config.file.upload_location
|
||||
@ -110,3 +116,28 @@ export function getCouponUrl(shop: string, couponId: string) {
|
||||
let subPath = `/coupon/${shop}`
|
||||
return `${config.file.show_url}${subPath}/${couponId}.png`
|
||||
}
|
||||
|
||||
export async function downloadRemoteFile(url: string) {
|
||||
const tempPath = os.tmpdir()
|
||||
const fileName = nanoid()
|
||||
const dest: string = `${tempPath}/${fileName}.png`
|
||||
if (jetpack.exists(dest)) {
|
||||
jetpack.remove(dest)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
url,
|
||||
responseType: 'stream',
|
||||
}).then(resp => {
|
||||
const writer = fs.createWriteStream(dest)
|
||||
resp.data.pipe(writer)
|
||||
writer.on('finish', () => {
|
||||
console.log('finish')
|
||||
resolve && resolve(dest)
|
||||
})
|
||||
writer.on('error', err => {
|
||||
reject && reject(err)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
21
src/services/TencentCDN.ts
Normal file
21
src/services/TencentCDN.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import axios from 'axios'
|
||||
import fs from 'fs'
|
||||
import FormData from 'form-data'
|
||||
|
||||
export function uploadToCDN(localPath: string, subPath: string) {
|
||||
let data = new FormData()
|
||||
data.append('sub_path', subPath)
|
||||
data.append('type', 'image')
|
||||
data.append('image-file', fs.createReadStream(localPath))
|
||||
|
||||
let config: any = {
|
||||
method: 'post',
|
||||
url: 'https://opm.kingsome.cn/api/upload',
|
||||
headers: {
|
||||
...data.getHeaders(),
|
||||
},
|
||||
data: data,
|
||||
}
|
||||
|
||||
return axios(config)
|
||||
}
|
20
src/utils/pinyin.ts
Normal file
20
src/utils/pinyin.ts
Normal file
File diff suppressed because one or more lines are too long
@ -36,3 +36,33 @@ export function md5(str) {
|
||||
str = md5sum.digest('hex')
|
||||
return str
|
||||
}
|
||||
|
||||
export function createSign(secretKey, paramStr, timestamp) {
|
||||
paramStr = `${paramStr}:${timestamp}:${secretKey}`
|
||||
return sha1(paramStr)
|
||||
}
|
||||
|
||||
export function checkSign({
|
||||
secretKey,
|
||||
data,
|
||||
timestamp,
|
||||
sign,
|
||||
signKeys,
|
||||
}: {
|
||||
secretKey: string
|
||||
data: {}
|
||||
timestamp: string
|
||||
sign: string
|
||||
signKeys: string[]
|
||||
}) {
|
||||
signKeys.sort()
|
||||
let signStr = ''
|
||||
for (let key of signKeys) {
|
||||
if (signStr.length > 0) {
|
||||
signStr += '&'
|
||||
}
|
||||
signStr += `${key}=${data[key]}`
|
||||
}
|
||||
let sign1 = createSign(secretKey, signStr, timestamp)
|
||||
return sign1 === sign
|
||||
}
|
||||
|
@ -68,3 +68,39 @@ export function isObjectId(id: string): boolean {
|
||||
//mongoose.Types.ObjectId.isValid(id)
|
||||
return /^[a-fA-F0-9]{24}$/.test(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 10进制 -> 62进制
|
||||
* @param {string | number} number
|
||||
* @return {string}
|
||||
*/
|
||||
export function string10to62(number: string | number) {
|
||||
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split('')
|
||||
const radix = chars.length
|
||||
let qutient = +number
|
||||
const arr = []
|
||||
do {
|
||||
const mod = qutient % radix
|
||||
qutient = (qutient - mod) / radix
|
||||
arr.unshift(chars[mod])
|
||||
} while (qutient)
|
||||
return arr.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 62进制 -> 10 进制
|
||||
* @param {string} numberCode
|
||||
* @return {number}
|
||||
*/
|
||||
export function string62to10(numberCode: string) {
|
||||
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'
|
||||
const radix = chars.length
|
||||
numberCode = numberCode + ''
|
||||
const len = numberCode.length
|
||||
let i = 0
|
||||
let originNumber = 0
|
||||
while (i < len) {
|
||||
originNumber += Math.pow(radix, i++) * (chars.indexOf(numberCode.charAt(len - i)) || 0)
|
||||
}
|
||||
return originNumber
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user